@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.
- package/{src/images/enrichImages.ts → dist/chunk-5HSVOF2J.js} +65 -53
- package/dist/chunk-5HSVOF2J.js.map +1 -0
- package/{src/iframeScript.ts → dist/chunk-5TYGSZAF.js} +259 -10
- package/dist/chunk-5TYGSZAF.js.map +1 -0
- package/{src/refine.ts → dist/chunk-GMJR2GXL.js} +30 -60
- package/dist/chunk-GMJR2GXL.js.map +1 -0
- package/dist/chunk-LQ65H4AO.js +41 -0
- package/dist/chunk-LQ65H4AO.js.map +1 -0
- package/{src/generate.ts → dist/chunk-PK26CWDO.js} +67 -108
- package/dist/chunk-PK26CWDO.js.map +1 -0
- package/dist/chunk-RTGCZUNJ.js +1 -0
- package/dist/chunk-RTGCZUNJ.js.map +1 -0
- package/dist/chunk-XM3D3TTJ.js +852 -0
- package/dist/chunk-XM3D3TTJ.js.map +1 -0
- package/dist/components.d.ts +57 -0
- package/dist/components.js +14 -0
- package/dist/components.js.map +1 -0
- package/dist/deploy.d.ts +39 -0
- package/dist/deploy.js +10 -0
- package/dist/deploy.js.map +1 -0
- package/dist/generate.d.ts +41 -0
- package/dist/generate.js +14 -0
- package/dist/generate.js.map +1 -0
- package/dist/images.d.ts +30 -0
- package/dist/images.js +14 -0
- package/dist/images.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/refine.d.ts +32 -0
- package/dist/refine.js +10 -0
- package/dist/refine.js.map +1 -0
- package/dist/themes-DOoj19c8.d.ts +35 -0
- package/dist/types-Flpl4wDs.d.ts +31 -0
- package/package.json +53 -50
- package/src/buildHtml.ts +0 -78
- package/src/components/Canvas.tsx +0 -162
- package/src/components/CodeEditor.tsx +0 -239
- package/src/components/FloatingToolbar.tsx +0 -350
- package/src/components/SectionList.tsx +0 -217
- package/src/components/index.ts +0 -4
- package/src/deploy.ts +0 -73
- package/src/images/dalleImages.ts +0 -29
- package/src/images/index.ts +0 -3
- package/src/images/pexels.ts +0 -27
- package/src/index.ts +0 -58
- package/src/themes.ts +0 -204
- 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
|
-
</>
|
|
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
|
-
}
|
package/src/components/index.ts
DELETED
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
|
-
}
|
package/src/images/index.ts
DELETED