@easybits.cloud/html-tailwind-generator 0.1.6 → 0.2.1
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/dist/chunk-24P7DHB7.js +41 -0
- package/dist/chunk-24P7DHB7.js.map +1 -0
- package/dist/chunk-464CGCFJ.js +912 -0
- package/dist/chunk-464CGCFJ.js.map +1 -0
- package/{src/refine.ts → dist/chunk-CB2LECVT.js} +30 -60
- package/dist/chunk-CB2LECVT.js.map +1 -0
- package/{src/images/dalleImages.ts → dist/chunk-LPI2QUCL.js} +10 -12
- package/dist/chunk-LPI2QUCL.js.map +1 -0
- package/{src/generate.ts → dist/chunk-S7YLW6ZU.js} +68 -115
- package/dist/chunk-S7YLW6ZU.js.map +1 -0
- package/{src/iframeScript.ts → dist/chunk-UGIQBLG5.js} +260 -10
- package/dist/chunk-UGIQBLG5.js.map +1 -0
- package/{src/images/enrichImages.ts → dist/chunk-YPK3DAFK.js} +44 -61
- package/dist/chunk-YPK3DAFK.js.map +1 -0
- package/dist/components.d.ts +65 -0
- package/dist/components.js +16 -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 +15 -0
- package/dist/images.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +67 -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-CWFZ6GB-.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/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,25 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findImageSlots,
|
|
3
|
+
searchImage
|
|
4
|
+
} from "./chunk-YPK3DAFK.js";
|
|
5
|
+
|
|
6
|
+
// src/generate.ts
|
|
1
7
|
import { streamText } from "ai";
|
|
2
8
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
3
9
|
import { nanoid } from "nanoid";
|
|
4
|
-
|
|
5
|
-
import { searchImage } from "./images/pexels";
|
|
6
|
-
import { generateImage } from "./images/dalleImages";
|
|
7
|
-
import type { Section3 } from "./types";
|
|
8
|
-
|
|
9
|
-
async function resolveModel(opts: { openaiApiKey?: string; anthropicApiKey?: string; modelId?: string; defaultOpenai: string; defaultAnthropic: string }) {
|
|
10
|
+
async function resolveModel(opts) {
|
|
10
11
|
const openaiKey = opts.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
11
12
|
if (openaiKey) {
|
|
12
13
|
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
13
14
|
const openai = createOpenAI({ apiKey: openaiKey });
|
|
14
15
|
return openai(opts.modelId || opts.defaultOpenai);
|
|
15
16
|
}
|
|
16
|
-
const anthropic = opts.anthropicApiKey
|
|
17
|
-
? createAnthropic({ apiKey: opts.anthropicApiKey })
|
|
18
|
-
: createAnthropic();
|
|
17
|
+
const anthropic = opts.anthropicApiKey ? createAnthropic({ apiKey: opts.anthropicApiKey }) : createAnthropic();
|
|
19
18
|
return anthropic(opts.modelId || opts.defaultAnthropic);
|
|
20
19
|
}
|
|
21
|
-
|
|
22
|
-
export const SYSTEM_PROMPT = `You are a world-class web designer who creates AWARD-WINNING landing pages. Your designs win Awwwards, FWA, and CSS Design Awards. You think in terms of visual hierarchy, whitespace, and emotional impact.
|
|
20
|
+
var SYSTEM_PROMPT = `You are a world-class web designer who creates AWARD-WINNING landing pages. Your designs win Awwwards, FWA, and CSS Design Awards. You think in terms of visual hierarchy, whitespace, and emotional impact.
|
|
23
21
|
|
|
24
22
|
RULES:
|
|
25
23
|
- Each section is a complete <section> tag with Tailwind CSS classes
|
|
@@ -28,24 +26,24 @@ RULES:
|
|
|
28
26
|
- Each section must be independent and self-contained
|
|
29
27
|
- Responsive: mobile-first with sm/md/lg/xl breakpoints
|
|
30
28
|
- All text content in Spanish unless the prompt specifies otherwise
|
|
31
|
-
- Use real-looking content (not Lorem ipsum)
|
|
29
|
+
- Use real-looking content (not Lorem ipsum) \u2014 make it specific to the prompt
|
|
32
30
|
|
|
33
|
-
IMAGES
|
|
31
|
+
IMAGES \u2014 CRITICAL:
|
|
34
32
|
- Use <img data-image-query="english search query" alt="description" class="..."/>
|
|
35
|
-
- NEVER include a src attribute
|
|
33
|
+
- NEVER include a src attribute \u2014 the system auto-replaces data-image-query with a real image URL
|
|
36
34
|
- For avatar-like elements, use colored divs with initials instead of img tags (e.g. <div class="w-10 h-10 rounded-full bg-primary flex items-center justify-center text-on-primary font-bold">JD</div>)
|
|
37
35
|
|
|
38
|
-
COLOR SYSTEM
|
|
36
|
+
COLOR SYSTEM \u2014 CRITICAL (READ CAREFULLY):
|
|
39
37
|
- Use semantic color classes: bg-primary, text-primary, bg-primary-light, bg-primary-dark, text-on-primary, bg-surface, bg-surface-alt, text-on-surface, text-on-surface-muted, bg-secondary, text-secondary, bg-accent, text-accent
|
|
40
38
|
- NEVER use hardcoded Tailwind color classes: NO bg-gray-*, bg-black, bg-white, bg-indigo-*, bg-blue-*, bg-purple-*, text-gray-*, text-black, text-white, etc.
|
|
41
39
|
- The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.
|
|
42
40
|
- ALL backgrounds MUST use: bg-primary, bg-primary-dark, bg-surface, bg-surface-alt
|
|
43
41
|
- ALL text MUST use: text-on-surface, text-on-surface-muted, text-on-primary, text-primary, text-accent
|
|
44
|
-
- CONTRAST RULE: on bg-primary or bg-primary-dark
|
|
42
|
+
- CONTRAST RULE: on bg-primary or bg-primary-dark \u2192 use text-on-primary. On bg-surface or bg-surface-alt \u2192 use text-on-surface or text-on-surface-muted. NEVER put text-on-surface on bg-primary or vice versa. text-accent is decorative \u2014 use sparingly on bg-surface/bg-surface-alt only.
|
|
45
43
|
- For gradients: from-primary to-primary-dark, from-surface to-surface-alt
|
|
46
44
|
- For hover: hover:bg-primary-dark, hover:bg-primary-light
|
|
47
45
|
|
|
48
|
-
DESIGN PHILOSOPHY
|
|
46
|
+
DESIGN PHILOSOPHY \u2014 what separates good from GREAT:
|
|
49
47
|
- WHITESPACE is your best friend. Generous padding (py-24, py-32, px-8). Let elements breathe.
|
|
50
48
|
- CONTRAST: mix dark sections with light ones. Alternate bg-primary and bg-surface sections.
|
|
51
49
|
- TYPOGRAPHY: use extreme size differences for hierarchy (text-7xl headline next to text-sm label)
|
|
@@ -54,37 +52,31 @@ DESIGN PHILOSOPHY — what separates good from GREAT:
|
|
|
54
52
|
- TEXTURE: use subtle patterns, gradients, border treatments, rounded-3xl mixed with sharp edges
|
|
55
53
|
- Each section should have a COMPLETELY DIFFERENT layout from the others
|
|
56
54
|
|
|
57
|
-
HERO SECTION
|
|
55
|
+
HERO SECTION \u2014 your masterpiece:
|
|
58
56
|
- Bento-grid or asymmetric layout, NOT a generic centered hero
|
|
59
57
|
- Large headline block + smaller stat/metric cards in a grid
|
|
60
58
|
- Real social proof: "2,847+ users", avatar stack (colored divs with initials), star ratings
|
|
61
59
|
- Bold oversized headline (text-6xl/7xl font-black leading-none)
|
|
62
60
|
- Tag/label above headline (uppercase, tracking-wider, text-xs)
|
|
63
|
-
- 2 CTAs: primary (large, with
|
|
61
|
+
- 2 CTAs: primary (large, with \u2192 arrow) + secondary (ghost/outlined)
|
|
64
62
|
- Real image via data-image-query
|
|
65
63
|
- Min height: min-h-[90vh] with generous padding
|
|
66
64
|
|
|
67
65
|
TAILWIND v3 NOTES:
|
|
68
66
|
- Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)
|
|
69
67
|
- Borders: border + border-gray-200 for visible borders`;
|
|
68
|
+
var PROMPT_SUFFIX = `
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
OUTPUT FORMAT: NDJSON — one JSON object per line, NO wrapper array, NO markdown fences.
|
|
70
|
+
OUTPUT FORMAT: NDJSON \u2014 one JSON object per line, NO wrapper array, NO markdown fences.
|
|
74
71
|
Each line: {"label": "Short Label", "html": "<section>...</section>"}
|
|
75
72
|
|
|
76
73
|
Generate 7-9 sections. Always start with Hero and end with Footer.
|
|
77
|
-
IMPORTANT: Make each section VISUALLY UNIQUE
|
|
74
|
+
IMPORTANT: Make each section VISUALLY UNIQUE \u2014 different layouts, different background colors, different grid structures.
|
|
78
75
|
Think like a premium design agency creating a $50K landing page.
|
|
79
76
|
NO generic Bootstrap layouts. Use creative grids, bento layouts, overlapping elements, asymmetric columns.`;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* Extract complete JSON objects from accumulated text using brace-depth tracking.
|
|
83
|
-
*/
|
|
84
|
-
export function extractJsonObjects(text: string): [any[], string] {
|
|
85
|
-
const objects: any[] = [];
|
|
77
|
+
function extractJsonObjects(text) {
|
|
78
|
+
const objects = [];
|
|
86
79
|
let remaining = text;
|
|
87
|
-
|
|
88
80
|
while (remaining.length > 0) {
|
|
89
81
|
remaining = remaining.trimStart();
|
|
90
82
|
if (!remaining.startsWith("{")) {
|
|
@@ -93,69 +85,45 @@ export function extractJsonObjects(text: string): [any[], string] {
|
|
|
93
85
|
remaining = remaining.slice(nextBrace);
|
|
94
86
|
continue;
|
|
95
87
|
}
|
|
96
|
-
|
|
97
88
|
let depth = 0;
|
|
98
89
|
let inString = false;
|
|
99
90
|
let escape = false;
|
|
100
91
|
let end = -1;
|
|
101
|
-
|
|
102
92
|
for (let i = 0; i < remaining.length; i++) {
|
|
103
93
|
const ch = remaining[i];
|
|
104
|
-
if (escape) {
|
|
105
|
-
|
|
106
|
-
|
|
94
|
+
if (escape) {
|
|
95
|
+
escape = false;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === "\\") {
|
|
99
|
+
escape = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (ch === '"') {
|
|
103
|
+
inString = !inString;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
107
106
|
if (inString) continue;
|
|
108
107
|
if (ch === "{") depth++;
|
|
109
|
-
if (ch === "}") {
|
|
108
|
+
if (ch === "}") {
|
|
109
|
+
depth--;
|
|
110
|
+
if (depth === 0) {
|
|
111
|
+
end = i;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
110
115
|
}
|
|
111
|
-
|
|
112
116
|
if (end === -1) break;
|
|
113
|
-
|
|
114
117
|
const candidate = remaining.slice(0, end + 1);
|
|
115
118
|
remaining = remaining.slice(end + 1);
|
|
116
|
-
|
|
117
119
|
try {
|
|
118
120
|
objects.push(JSON.parse(candidate));
|
|
119
121
|
} catch {
|
|
120
|
-
// malformed, skip
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
|
-
|
|
124
124
|
return [objects, remaining];
|
|
125
125
|
}
|
|
126
|
-
|
|
127
|
-
export interface GenerateOptions {
|
|
128
|
-
/** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */
|
|
129
|
-
anthropicApiKey?: string;
|
|
130
|
-
/** OpenAI API key. If provided, uses GPT-4o instead of Claude */
|
|
131
|
-
openaiApiKey?: string;
|
|
132
|
-
/** Landing page description prompt */
|
|
133
|
-
prompt: string;
|
|
134
|
-
/** Reference image (base64 data URI) for vision-based generation */
|
|
135
|
-
referenceImage?: string;
|
|
136
|
-
/** Extra instructions appended to the prompt */
|
|
137
|
-
extraInstructions?: string;
|
|
138
|
-
/** Custom system prompt (overrides default SYSTEM_PROMPT) */
|
|
139
|
-
systemPrompt?: string;
|
|
140
|
-
/** Model ID (default: gpt-4o for OpenAI, claude-sonnet-4-6 for Anthropic) */
|
|
141
|
-
model?: string;
|
|
142
|
-
/** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */
|
|
143
|
-
pexelsApiKey?: string;
|
|
144
|
-
/** Called when a new section is parsed from the stream */
|
|
145
|
-
onSection?: (section: Section3) => void;
|
|
146
|
-
/** Called when a section's images are enriched */
|
|
147
|
-
onImageUpdate?: (sectionId: string, html: string) => void;
|
|
148
|
-
/** Called when generation is complete */
|
|
149
|
-
onDone?: (sections: Section3[]) => void;
|
|
150
|
-
/** Called on error */
|
|
151
|
-
onError?: (error: Error) => void;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Generate a landing page with streaming AI + image enrichment.
|
|
156
|
-
* Returns all generated sections when complete.
|
|
157
|
-
*/
|
|
158
|
-
export async function generateLanding(options: GenerateOptions): Promise<Section3[]> {
|
|
126
|
+
async function generateLanding(options) {
|
|
159
127
|
const {
|
|
160
128
|
anthropicApiKey,
|
|
161
129
|
openaiApiKey: _openaiApiKey,
|
|
@@ -168,60 +136,49 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
|
|
|
168
136
|
onSection,
|
|
169
137
|
onImageUpdate,
|
|
170
138
|
onDone,
|
|
171
|
-
onError
|
|
139
|
+
onError
|
|
172
140
|
} = options;
|
|
173
|
-
|
|
174
141
|
const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;
|
|
175
142
|
const model = await resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai: "gpt-4o", defaultAnthropic: "claude-sonnet-4-6" });
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
const content: any[] = [];
|
|
143
|
+
const extra = extraInstructions ? `
|
|
144
|
+
Additional instructions: ${extraInstructions}` : "";
|
|
145
|
+
const content = [];
|
|
180
146
|
if (referenceImage) {
|
|
181
147
|
content.push({ type: "image", image: referenceImage });
|
|
182
148
|
content.push({
|
|
183
149
|
type: "text",
|
|
184
|
-
text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}
|
|
150
|
+
text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}`
|
|
185
151
|
});
|
|
186
152
|
} else {
|
|
187
153
|
content.push({
|
|
188
154
|
type: "text",
|
|
189
|
-
text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}
|
|
155
|
+
text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}`
|
|
190
156
|
});
|
|
191
157
|
}
|
|
192
|
-
|
|
193
158
|
const result = streamText({
|
|
194
159
|
model,
|
|
195
160
|
system: systemPrompt,
|
|
196
|
-
messages: [{ role: "user", content }]
|
|
161
|
+
messages: [{ role: "user", content }]
|
|
197
162
|
});
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
const imagePromises: Promise<void>[] = [];
|
|
163
|
+
const allSections = [];
|
|
164
|
+
const imagePromises = [];
|
|
201
165
|
let sectionOrder = 0;
|
|
202
166
|
let buffer = "";
|
|
203
|
-
|
|
204
167
|
try {
|
|
205
168
|
for await (const chunk of result.textStream) {
|
|
206
169
|
buffer += chunk;
|
|
207
|
-
|
|
208
170
|
const [objects, remaining] = extractJsonObjects(buffer);
|
|
209
171
|
buffer = remaining;
|
|
210
|
-
|
|
211
172
|
for (const obj of objects) {
|
|
212
173
|
if (!obj.html || !obj.label) continue;
|
|
213
|
-
|
|
214
|
-
const section: Section3 = {
|
|
174
|
+
const section = {
|
|
215
175
|
id: nanoid(8),
|
|
216
176
|
order: sectionOrder++,
|
|
217
177
|
html: obj.html,
|
|
218
|
-
label: obj.label
|
|
178
|
+
label: obj.label
|
|
219
179
|
};
|
|
220
|
-
|
|
221
180
|
allSections.push(section);
|
|
222
181
|
onSection?.(section);
|
|
223
|
-
|
|
224
|
-
// Enrich images (DALL-E if openaiApiKey, otherwise Pexels)
|
|
225
182
|
const slots = findImageSlots(section.html);
|
|
226
183
|
if (slots.length > 0) {
|
|
227
184
|
const sectionRef = section;
|
|
@@ -230,14 +187,9 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
|
|
|
230
187
|
(async () => {
|
|
231
188
|
const results = await Promise.allSettled(
|
|
232
189
|
slotsSnapshot.map(async (slot) => {
|
|
233
|
-
let url
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
if (!url) {
|
|
238
|
-
const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
|
|
239
|
-
url = img?.url || null;
|
|
240
|
-
}
|
|
190
|
+
let url = null;
|
|
191
|
+
const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
|
|
192
|
+
url = img?.url || null;
|
|
241
193
|
url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
|
|
242
194
|
return { slot, url };
|
|
243
195
|
})
|
|
@@ -259,37 +211,38 @@ export async function generateLanding(options: GenerateOptions): Promise<Section
|
|
|
259
211
|
}
|
|
260
212
|
}
|
|
261
213
|
}
|
|
262
|
-
|
|
263
|
-
// Parse remaining buffer
|
|
264
214
|
if (buffer.trim()) {
|
|
265
215
|
let cleaned = buffer.trim();
|
|
266
216
|
if (cleaned.startsWith("```")) {
|
|
267
|
-
cleaned = cleaned
|
|
268
|
-
.replace(/^```(?:json)?\s*/, "")
|
|
269
|
-
.replace(/\s*```$/, "");
|
|
217
|
+
cleaned = cleaned.replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
|
|
270
218
|
}
|
|
271
219
|
const [lastObjects] = extractJsonObjects(cleaned);
|
|
272
220
|
for (const obj of lastObjects) {
|
|
273
221
|
if (!obj.html || !obj.label) continue;
|
|
274
|
-
const section
|
|
222
|
+
const section = {
|
|
275
223
|
id: nanoid(8),
|
|
276
224
|
order: sectionOrder++,
|
|
277
225
|
html: obj.html,
|
|
278
|
-
label: obj.label
|
|
226
|
+
label: obj.label
|
|
279
227
|
};
|
|
280
228
|
allSections.push(section);
|
|
281
229
|
onSection?.(section);
|
|
282
230
|
}
|
|
283
231
|
}
|
|
284
|
-
|
|
285
|
-
// Wait for image enrichment
|
|
286
232
|
await Promise.allSettled(imagePromises);
|
|
287
|
-
|
|
288
233
|
onDone?.(allSections);
|
|
289
234
|
return allSections;
|
|
290
|
-
} catch (err
|
|
235
|
+
} catch (err) {
|
|
291
236
|
const error = err instanceof Error ? err : new Error(err?.message || "Generation failed");
|
|
292
237
|
onError?.(error);
|
|
293
238
|
throw error;
|
|
294
239
|
}
|
|
295
240
|
}
|
|
241
|
+
|
|
242
|
+
export {
|
|
243
|
+
SYSTEM_PROMPT,
|
|
244
|
+
PROMPT_SUFFIX,
|
|
245
|
+
extractJsonObjects,
|
|
246
|
+
generateLanding
|
|
247
|
+
};
|
|
248
|
+
//# sourceMappingURL=chunk-S7YLW6ZU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/generate.ts"],"sourcesContent":["import { streamText } from \"ai\";\nimport { createAnthropic } from \"@ai-sdk/anthropic\";\nimport { nanoid } from \"nanoid\";\nimport { findImageSlots } from \"./images/enrichImages\";\nimport { searchImage } from \"./images/pexels\";\nimport { generateImage } from \"./images/dalleImages\";\nimport type { Section3 } from \"./types\";\n\nasync function resolveModel(opts: { openaiApiKey?: string; anthropicApiKey?: string; modelId?: string; defaultOpenai: string; defaultAnthropic: string }) {\n const openaiKey = opts.openaiApiKey || process.env.OPENAI_API_KEY;\n if (openaiKey) {\n const { createOpenAI } = await import(\"@ai-sdk/openai\");\n const openai = createOpenAI({ apiKey: openaiKey });\n return openai(opts.modelId || opts.defaultOpenai);\n }\n const anthropic = opts.anthropicApiKey\n ? createAnthropic({ apiKey: opts.anthropicApiKey })\n : createAnthropic();\n return anthropic(opts.modelId || opts.defaultAnthropic);\n}\n\nexport const SYSTEM_PROMPT = `You are a world-class web designer who creates AWARD-WINNING landing pages. Your designs win Awwwards, FWA, and CSS Design Awards. You think in terms of visual hierarchy, whitespace, and emotional impact.\n\nRULES:\n- Each section is a complete <section> tag with Tailwind CSS classes\n- Use Tailwind CDN classes ONLY (no custom CSS, no @apply, no @import, no @tailwind directives)\n- NO JavaScript, only HTML+Tailwind\n- Each section must be independent and self-contained\n- Responsive: mobile-first with sm/md/lg/xl breakpoints\n- All text content in Spanish unless the prompt specifies otherwise\n- Use real-looking content (not Lorem ipsum) — make it specific to the prompt\n\nIMAGES — CRITICAL:\n- Use <img data-image-query=\"english search query\" alt=\"description\" class=\"...\"/>\n- NEVER include a src attribute — the system auto-replaces data-image-query with a real image URL\n- For avatar-like elements, use colored divs with initials instead of img tags (e.g. <div class=\"w-10 h-10 rounded-full bg-primary flex items-center justify-center text-on-primary font-bold\">JD</div>)\n\nCOLOR SYSTEM — CRITICAL (READ CAREFULLY):\n- Use semantic color classes: bg-primary, text-primary, bg-primary-light, bg-primary-dark, text-on-primary, bg-surface, bg-surface-alt, text-on-surface, text-on-surface-muted, bg-secondary, text-secondary, bg-accent, text-accent\n- NEVER use hardcoded Tailwind color classes: NO bg-gray-*, bg-black, bg-white, bg-indigo-*, bg-blue-*, bg-purple-*, text-gray-*, text-black, text-white, etc.\n- The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.\n- ALL backgrounds MUST use: bg-primary, bg-primary-dark, bg-surface, bg-surface-alt\n- ALL text MUST use: text-on-surface, text-on-surface-muted, text-on-primary, text-primary, text-accent\n- CONTRAST RULE: on bg-primary or bg-primary-dark → use text-on-primary. On bg-surface or bg-surface-alt → use text-on-surface or text-on-surface-muted. NEVER put text-on-surface on bg-primary or vice versa. text-accent is decorative — use sparingly on bg-surface/bg-surface-alt only.\n- For gradients: from-primary to-primary-dark, from-surface to-surface-alt\n- For hover: hover:bg-primary-dark, hover:bg-primary-light\n\nDESIGN PHILOSOPHY — what separates good from GREAT:\n- WHITESPACE is your best friend. Generous padding (py-24, py-32, px-8). Let elements breathe.\n- CONTRAST: mix dark sections with light ones. Alternate bg-primary and bg-surface sections.\n- TYPOGRAPHY: use extreme size differences for hierarchy (text-7xl headline next to text-sm label)\n- DEPTH: overlapping elements, negative margins (-mt-12), z-index layering, shadows\n- ASYMMETRY: avoid centering everything. Use grid-cols-5 with col-span-3 + col-span-2. Offset elements.\n- TEXTURE: use subtle patterns, gradients, border treatments, rounded-3xl mixed with sharp edges\n- Each section should have a COMPLETELY DIFFERENT layout from the others\n\nHERO SECTION — your masterpiece:\n- Bento-grid or asymmetric layout, NOT a generic centered hero\n- Large headline block + smaller stat/metric cards in a grid\n- Real social proof: \"2,847+ users\", avatar stack (colored divs with initials), star ratings\n- Bold oversized headline (text-6xl/7xl font-black leading-none)\n- Tag/label above headline (uppercase, tracking-wider, text-xs)\n- 2 CTAs: primary (large, with → arrow) + secondary (ghost/outlined)\n- Real image via data-image-query\n- Min height: min-h-[90vh] with generous padding\n\nTAILWIND v3 NOTES:\n- Standard Tailwind v3 classes (shadow-sm, shadow-md, rounded-md, etc.)\n- Borders: border + border-gray-200 for visible borders`;\n\nexport const PROMPT_SUFFIX = `\n\nOUTPUT FORMAT: NDJSON — one JSON object per line, NO wrapper array, NO markdown fences.\nEach line: {\"label\": \"Short Label\", \"html\": \"<section>...</section>\"}\n\nGenerate 7-9 sections. Always start with Hero and end with Footer.\nIMPORTANT: Make each section VISUALLY UNIQUE — different layouts, different background colors, different grid structures.\nThink like a premium design agency creating a $50K landing page.\nNO generic Bootstrap layouts. Use creative grids, bento layouts, overlapping elements, asymmetric columns.`;\n\n/**\n * Extract complete JSON objects from accumulated text using brace-depth tracking.\n */\nexport function extractJsonObjects(text: string): [any[], string] {\n const objects: any[] = [];\n let remaining = text;\n\n while (remaining.length > 0) {\n remaining = remaining.trimStart();\n if (!remaining.startsWith(\"{\")) {\n const nextBrace = remaining.indexOf(\"{\");\n if (nextBrace === -1) break;\n remaining = remaining.slice(nextBrace);\n continue;\n }\n\n let depth = 0;\n let inString = false;\n let escape = false;\n let end = -1;\n\n for (let i = 0; i < remaining.length; i++) {\n const ch = remaining[i];\n if (escape) { escape = false; continue; }\n if (ch === \"\\\\\") { escape = true; continue; }\n if (ch === '\"') { inString = !inString; continue; }\n if (inString) continue;\n if (ch === \"{\") depth++;\n if (ch === \"}\") { depth--; if (depth === 0) { end = i; break; } }\n }\n\n if (end === -1) break;\n\n const candidate = remaining.slice(0, end + 1);\n remaining = remaining.slice(end + 1);\n\n try {\n objects.push(JSON.parse(candidate));\n } catch {\n // malformed, skip\n }\n }\n\n return [objects, remaining];\n}\n\nexport interface GenerateOptions {\n /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */\n anthropicApiKey?: string;\n /** OpenAI API key. If provided, uses GPT-4o instead of Claude */\n openaiApiKey?: string;\n /** Landing page description prompt */\n prompt: string;\n /** Reference image (base64 data URI) for vision-based generation */\n referenceImage?: string;\n /** Extra instructions appended to the prompt */\n extraInstructions?: string;\n /** Custom system prompt (overrides default SYSTEM_PROMPT) */\n systemPrompt?: string;\n /** Model ID (default: gpt-4o for OpenAI, claude-sonnet-4-6 for Anthropic) */\n model?: string;\n /** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */\n pexelsApiKey?: string;\n /** Called when a new section is parsed from the stream */\n onSection?: (section: Section3) => void;\n /** Called when a section's images are enriched */\n onImageUpdate?: (sectionId: string, html: string) => void;\n /** Called when generation is complete */\n onDone?: (sections: Section3[]) => void;\n /** Called on error */\n onError?: (error: Error) => void;\n}\n\n/**\n * Generate a landing page with streaming AI + image enrichment.\n * Returns all generated sections when complete.\n */\nexport async function generateLanding(options: GenerateOptions): Promise<Section3[]> {\n const {\n anthropicApiKey,\n openaiApiKey: _openaiApiKey,\n prompt,\n referenceImage,\n extraInstructions,\n systemPrompt = SYSTEM_PROMPT,\n model: modelId,\n pexelsApiKey,\n onSection,\n onImageUpdate,\n onDone,\n onError,\n } = options;\n\n const openaiApiKey = _openaiApiKey || process.env.OPENAI_API_KEY;\n const model = await resolveModel({ openaiApiKey, anthropicApiKey, modelId, defaultOpenai: \"gpt-4o\", defaultAnthropic: \"claude-sonnet-4-6\" });\n\n // Build prompt content (supports multimodal with reference image)\n const extra = extraInstructions ? `\\nAdditional instructions: ${extraInstructions}` : \"\";\n const content: any[] = [];\n if (referenceImage) {\n content.push({ type: \"image\", image: referenceImage });\n content.push({\n type: \"text\",\n text: `Generate a landing page inspired by this reference image for: ${prompt}${extra}${PROMPT_SUFFIX}`,\n });\n } else {\n content.push({\n type: \"text\",\n text: `Generate a landing page for: ${prompt}${extra}${PROMPT_SUFFIX}`,\n });\n }\n\n const result = streamText({\n model,\n system: systemPrompt,\n messages: [{ role: \"user\", content }],\n });\n\n const allSections: Section3[] = [];\n const imagePromises: Promise<void>[] = [];\n let sectionOrder = 0;\n let buffer = \"\";\n\n try {\n for await (const chunk of result.textStream) {\n buffer += chunk;\n\n const [objects, remaining] = extractJsonObjects(buffer);\n buffer = remaining;\n\n for (const obj of objects) {\n if (!obj.html || !obj.label) continue;\n\n const section: Section3 = {\n id: nanoid(8),\n order: sectionOrder++,\n html: obj.html,\n label: obj.label,\n };\n\n allSections.push(section);\n onSection?.(section);\n\n // Enrich images (DALL-E if openaiApiKey, otherwise Pexels)\n const slots = findImageSlots(section.html);\n if (slots.length > 0) {\n const sectionRef = section;\n const slotsSnapshot = slots.map((s) => ({ ...s }));\n imagePromises.push(\n (async () => {\n const results = await Promise.allSettled(\n slotsSnapshot.map(async (slot) => {\n let url: string | null = null;\n // Pexels first (permanent URLs), DALL-E disabled (temporary URLs expire ~2hrs)\n const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);\n url = img?.url || null;\n url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;\n return { slot, url };\n })\n );\n let html = sectionRef.html;\n for (const r of results) {\n if (r.status === \"fulfilled\" && r.value) {\n const { slot, url } = r.value;\n const replacement = slot.replaceStr.replace(\"{url}\", url);\n html = html.replaceAll(slot.searchStr, replacement);\n }\n }\n if (html !== sectionRef.html) {\n sectionRef.html = html;\n onImageUpdate?.(sectionRef.id, html);\n }\n })()\n );\n }\n }\n }\n\n // Parse remaining buffer\n if (buffer.trim()) {\n let cleaned = buffer.trim();\n if (cleaned.startsWith(\"```\")) {\n cleaned = cleaned\n .replace(/^```(?:json)?\\s*/, \"\")\n .replace(/\\s*```$/, \"\");\n }\n const [lastObjects] = extractJsonObjects(cleaned);\n for (const obj of lastObjects) {\n if (!obj.html || !obj.label) continue;\n const section: Section3 = {\n id: nanoid(8),\n order: sectionOrder++,\n html: obj.html,\n label: obj.label,\n };\n allSections.push(section);\n onSection?.(section);\n }\n }\n\n // Wait for image enrichment\n await Promise.allSettled(imagePromises);\n\n onDone?.(allSections);\n return allSections;\n } catch (err: any) {\n const error = err instanceof Error ? err : new Error(err?.message || \"Generation failed\");\n onError?.(error);\n throw error;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,10 +1,173 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// src/themes.ts
|
|
2
|
+
var LANDING_THEMES = [
|
|
3
|
+
{
|
|
4
|
+
id: "default",
|
|
5
|
+
label: "Neutral",
|
|
6
|
+
colors: {
|
|
7
|
+
primary: "#18181b",
|
|
8
|
+
"primary-light": "#3f3f46",
|
|
9
|
+
"primary-dark": "#09090b",
|
|
10
|
+
secondary: "#a1a1aa",
|
|
11
|
+
accent: "#18181b",
|
|
12
|
+
surface: "#ffffff",
|
|
13
|
+
"surface-alt": "#fafafa",
|
|
14
|
+
"on-surface": "#18181b",
|
|
15
|
+
"on-surface-muted": "#71717a",
|
|
16
|
+
"on-primary": "#ffffff"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "dark",
|
|
21
|
+
label: "Dark",
|
|
22
|
+
colors: {
|
|
23
|
+
primary: "#e4e4e7",
|
|
24
|
+
"primary-light": "#f4f4f5",
|
|
25
|
+
"primary-dark": "#a1a1aa",
|
|
26
|
+
secondary: "#71717a",
|
|
27
|
+
accent: "#a78bfa",
|
|
28
|
+
surface: "#09090b",
|
|
29
|
+
"surface-alt": "#18181b",
|
|
30
|
+
"on-surface": "#fafafa",
|
|
31
|
+
"on-surface-muted": "#a1a1aa",
|
|
32
|
+
"on-primary": "#09090b"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "slate",
|
|
37
|
+
label: "Slate",
|
|
38
|
+
colors: {
|
|
39
|
+
primary: "#3b82f6",
|
|
40
|
+
"primary-light": "#60a5fa",
|
|
41
|
+
"primary-dark": "#2563eb",
|
|
42
|
+
secondary: "#64748b",
|
|
43
|
+
accent: "#3b82f6",
|
|
44
|
+
surface: "#ffffff",
|
|
45
|
+
"surface-alt": "#f8fafc",
|
|
46
|
+
"on-surface": "#0f172a",
|
|
47
|
+
"on-surface-muted": "#64748b",
|
|
48
|
+
"on-primary": "#ffffff"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "midnight",
|
|
53
|
+
label: "Midnight",
|
|
54
|
+
colors: {
|
|
55
|
+
primary: "#6366f1",
|
|
56
|
+
"primary-light": "#818cf8",
|
|
57
|
+
"primary-dark": "#4f46e5",
|
|
58
|
+
secondary: "#94a3b8",
|
|
59
|
+
accent: "#a78bfa",
|
|
60
|
+
surface: "#0f172a",
|
|
61
|
+
"surface-alt": "#1e293b",
|
|
62
|
+
"on-surface": "#e2e8f0",
|
|
63
|
+
"on-surface-muted": "#94a3b8",
|
|
64
|
+
"on-primary": "#ffffff"
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "warm",
|
|
69
|
+
label: "Warm",
|
|
70
|
+
colors: {
|
|
71
|
+
primary: "#b45309",
|
|
72
|
+
"primary-light": "#d97706",
|
|
73
|
+
"primary-dark": "#92400e",
|
|
74
|
+
secondary: "#78716c",
|
|
75
|
+
accent: "#b45309",
|
|
76
|
+
surface: "#fafaf9",
|
|
77
|
+
"surface-alt": "#f5f5f4",
|
|
78
|
+
"on-surface": "#1c1917",
|
|
79
|
+
"on-surface-muted": "#78716c",
|
|
80
|
+
"on-primary": "#ffffff"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
function parseHex(hex) {
|
|
85
|
+
return {
|
|
86
|
+
r: parseInt(hex.slice(1, 3), 16),
|
|
87
|
+
g: parseInt(hex.slice(3, 5), 16),
|
|
88
|
+
b: parseInt(hex.slice(5, 7), 16)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function toHex(r, g, b) {
|
|
92
|
+
return `#${[r, g, b].map((c) => Math.max(0, Math.min(255, c)).toString(16).padStart(2, "0")).join("")}`;
|
|
93
|
+
}
|
|
94
|
+
function luminance(hex) {
|
|
95
|
+
const { r, g, b } = parseHex(hex);
|
|
96
|
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
97
|
+
}
|
|
98
|
+
function lighten(hex, amount = 40) {
|
|
99
|
+
const { r, g, b } = parseHex(hex);
|
|
100
|
+
return toHex(r + amount, g + amount, b + amount);
|
|
101
|
+
}
|
|
102
|
+
function darken(hex, amount = 40) {
|
|
103
|
+
const { r, g, b } = parseHex(hex);
|
|
104
|
+
return toHex(r - amount, g - amount, b - amount);
|
|
105
|
+
}
|
|
106
|
+
function buildCustomTheme(colors) {
|
|
107
|
+
const { primary, secondary = "#f59e0b", accent = "#06b6d4", surface = "#ffffff" } = colors;
|
|
108
|
+
const onPrimary = luminance(primary) > 0.5 ? "#111827" : "#ffffff";
|
|
109
|
+
const surfaceLum = luminance(surface);
|
|
110
|
+
const isDarkSurface = surfaceLum < 0.5;
|
|
111
|
+
return {
|
|
112
|
+
id: "custom",
|
|
113
|
+
label: "Custom",
|
|
114
|
+
colors: {
|
|
115
|
+
primary,
|
|
116
|
+
"primary-light": lighten(primary),
|
|
117
|
+
"primary-dark": darken(primary),
|
|
118
|
+
secondary,
|
|
119
|
+
accent,
|
|
120
|
+
surface,
|
|
121
|
+
"surface-alt": isDarkSurface ? lighten(surface, 20) : darken(surface, 5),
|
|
122
|
+
"on-surface": isDarkSurface ? "#f1f5f9" : "#111827",
|
|
123
|
+
"on-surface-muted": isDarkSurface ? "#94a3b8" : "#6b7280",
|
|
124
|
+
"on-primary": onPrimary
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function buildCustomThemeCss(colors) {
|
|
129
|
+
const theme = buildCustomTheme(colors);
|
|
130
|
+
return `[data-theme="custom"] {
|
|
131
|
+
${buildCssVars(theme.colors)}
|
|
132
|
+
}`;
|
|
133
|
+
}
|
|
134
|
+
function buildCssVars(colors) {
|
|
135
|
+
return Object.entries(colors).map(([k, v]) => ` --color-${k}: ${v};`).join("\n");
|
|
136
|
+
}
|
|
137
|
+
function buildTailwindConfig() {
|
|
138
|
+
const colorEntries = Object.keys(LANDING_THEMES[0].colors).map((k) => ` '${k}': 'var(--color-${k})'`).join(",\n");
|
|
139
|
+
return `{
|
|
140
|
+
theme: {
|
|
141
|
+
extend: {
|
|
142
|
+
colors: {
|
|
143
|
+
${colorEntries}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}`;
|
|
148
|
+
}
|
|
149
|
+
function buildThemeCss() {
|
|
150
|
+
const defaultTheme = LANDING_THEMES[0];
|
|
151
|
+
const overrides = LANDING_THEMES.slice(1).map((t) => `[data-theme="${t.id}"] {
|
|
152
|
+
${buildCssVars(t.colors)}
|
|
153
|
+
}`).join("\n\n");
|
|
154
|
+
const css = `:root {
|
|
155
|
+
${buildCssVars(defaultTheme.colors)}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
${overrides}`;
|
|
159
|
+
return { css, tailwindConfig: buildTailwindConfig() };
|
|
160
|
+
}
|
|
161
|
+
function buildSingleThemeCss(themeId) {
|
|
162
|
+
const theme = LANDING_THEMES.find((t) => t.id === themeId) || LANDING_THEMES[0];
|
|
163
|
+
const css = `:root {
|
|
164
|
+
${buildCssVars(theme.colors)}
|
|
165
|
+
}`;
|
|
166
|
+
return { css, tailwindConfig: buildTailwindConfig() };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/iframeScript.ts
|
|
170
|
+
function getIframeScript() {
|
|
8
171
|
return `
|
|
9
172
|
(function() {
|
|
10
173
|
let hoveredEl = null;
|
|
@@ -70,7 +233,7 @@ export function getIframeScript(): string {
|
|
|
70
233
|
hoveredEl = null;
|
|
71
234
|
});
|
|
72
235
|
|
|
73
|
-
// Click
|
|
236
|
+
// Click \u2014 select element
|
|
74
237
|
document.addEventListener('click', function(e) {
|
|
75
238
|
e.preventDefault();
|
|
76
239
|
e.stopPropagation();
|
|
@@ -120,7 +283,7 @@ export function getIframeScript(): string {
|
|
|
120
283
|
}, '*');
|
|
121
284
|
}, true);
|
|
122
285
|
|
|
123
|
-
// Double-click
|
|
286
|
+
// Double-click \u2014 contentEditable for text
|
|
124
287
|
document.addEventListener('dblclick', function(e) {
|
|
125
288
|
e.preventDefault();
|
|
126
289
|
e.stopPropagation();
|
|
@@ -178,7 +341,19 @@ export function getIframeScript(): string {
|
|
|
178
341
|
|
|
179
342
|
if (msg.action === 'update-section') {
|
|
180
343
|
var el = getSectionElement(msg.id);
|
|
181
|
-
if (el
|
|
344
|
+
if (el && typeof window.morphdom === 'function') {
|
|
345
|
+
var tmp = document.createElement('div');
|
|
346
|
+
tmp.innerHTML = msg.html;
|
|
347
|
+
window.morphdom(el, tmp, {
|
|
348
|
+
childrenOnly: true,
|
|
349
|
+
onBeforeElUpdated: function(fromEl, toEl) {
|
|
350
|
+
if (fromEl.isEqualNode(toEl)) return false;
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
} else if (el) {
|
|
355
|
+
el.innerHTML = msg.html;
|
|
356
|
+
}
|
|
182
357
|
}
|
|
183
358
|
|
|
184
359
|
if (msg.action === 'remove-section') {
|
|
@@ -259,3 +434,78 @@ export function getIframeScript(): string {
|
|
|
259
434
|
})();
|
|
260
435
|
`;
|
|
261
436
|
}
|
|
437
|
+
|
|
438
|
+
// src/buildHtml.ts
|
|
439
|
+
function buildPreviewHtml(sections, theme) {
|
|
440
|
+
const sorted = [...sections].sort((a, b) => a.order - b.order);
|
|
441
|
+
const body = sorted.map((s) => `<div data-section-id="${s.id}">${s.html}</div>`).join("\n");
|
|
442
|
+
const dataTheme = theme && theme !== "default" ? ` data-theme="${theme}"` : "";
|
|
443
|
+
const { css, tailwindConfig } = buildThemeCss();
|
|
444
|
+
return `<!DOCTYPE html>
|
|
445
|
+
<html lang="es"${dataTheme}>
|
|
446
|
+
<head>
|
|
447
|
+
<meta charset="UTF-8"/>
|
|
448
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
449
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
450
|
+
<script src="https://unpkg.com/morphdom@2.7.4/dist/morphdom-umd.min.js"></script>
|
|
451
|
+
<script>tailwind.config = ${tailwindConfig}</script>
|
|
452
|
+
<style>
|
|
453
|
+
${css}
|
|
454
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
455
|
+
html{scroll-behavior:smooth}
|
|
456
|
+
body{font-family:system-ui,-apple-system,sans-serif;background-color:var(--color-surface);color:var(--color-on-surface)}
|
|
457
|
+
img{max-width:100%}
|
|
458
|
+
[contenteditable="true"]{cursor:text}
|
|
459
|
+
</style>
|
|
460
|
+
</head>
|
|
461
|
+
<body class="bg-surface text-on-surface">
|
|
462
|
+
${body}
|
|
463
|
+
<script>${getIframeScript()}</script>
|
|
464
|
+
</body>
|
|
465
|
+
</html>`;
|
|
466
|
+
}
|
|
467
|
+
function buildDeployHtml(sections, theme, customColors) {
|
|
468
|
+
const sorted = [...sections].sort((a, b) => a.order - b.order);
|
|
469
|
+
const body = sorted.map((s) => s.html).join("\n");
|
|
470
|
+
const isCustom = theme === "custom" && customColors;
|
|
471
|
+
const dataTheme = theme && theme !== "default" && !isCustom ? ` data-theme="${theme}"` : "";
|
|
472
|
+
const { css: baseCss, tailwindConfig } = isCustom ? (() => {
|
|
473
|
+
const ct = buildCustomTheme(customColors);
|
|
474
|
+
const vars = Object.entries(ct.colors).map(([k, v]) => ` --color-${k}: ${v};`).join("\n");
|
|
475
|
+
return { css: `:root {
|
|
476
|
+
${vars}
|
|
477
|
+
}`, tailwindConfig: buildSingleThemeCss("default").tailwindConfig };
|
|
478
|
+
})() : buildSingleThemeCss(theme || "default");
|
|
479
|
+
return `<!DOCTYPE html>
|
|
480
|
+
<html lang="es"${dataTheme}>
|
|
481
|
+
<head>
|
|
482
|
+
<meta charset="UTF-8"/>
|
|
483
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
484
|
+
<title>Landing Page</title>
|
|
485
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
486
|
+
<script>tailwind.config = ${tailwindConfig}</script>
|
|
487
|
+
<style>
|
|
488
|
+
${baseCss}
|
|
489
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
490
|
+
html{scroll-behavior:smooth}
|
|
491
|
+
body{font-family:system-ui,-apple-system,sans-serif;background-color:var(--color-surface);color:var(--color-on-surface)}
|
|
492
|
+
section > * {max-width:80rem;margin-left:auto;margin-right:auto}
|
|
493
|
+
</style>
|
|
494
|
+
</head>
|
|
495
|
+
<body class="bg-surface text-on-surface">
|
|
496
|
+
${body}
|
|
497
|
+
</body>
|
|
498
|
+
</html>`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export {
|
|
502
|
+
LANDING_THEMES,
|
|
503
|
+
buildCustomTheme,
|
|
504
|
+
buildCustomThemeCss,
|
|
505
|
+
buildThemeCss,
|
|
506
|
+
buildSingleThemeCss,
|
|
507
|
+
getIframeScript,
|
|
508
|
+
buildPreviewHtml,
|
|
509
|
+
buildDeployHtml
|
|
510
|
+
};
|
|
511
|
+
//# sourceMappingURL=chunk-UGIQBLG5.js.map
|