@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.
Files changed (47) hide show
  1. package/dist/chunk-24P7DHB7.js +41 -0
  2. package/dist/chunk-24P7DHB7.js.map +1 -0
  3. package/dist/chunk-464CGCFJ.js +912 -0
  4. package/dist/chunk-464CGCFJ.js.map +1 -0
  5. package/{src/refine.ts → dist/chunk-CB2LECVT.js} +30 -60
  6. package/dist/chunk-CB2LECVT.js.map +1 -0
  7. package/{src/images/dalleImages.ts → dist/chunk-LPI2QUCL.js} +10 -12
  8. package/dist/chunk-LPI2QUCL.js.map +1 -0
  9. package/{src/generate.ts → dist/chunk-S7YLW6ZU.js} +68 -115
  10. package/dist/chunk-S7YLW6ZU.js.map +1 -0
  11. package/{src/iframeScript.ts → dist/chunk-UGIQBLG5.js} +260 -10
  12. package/dist/chunk-UGIQBLG5.js.map +1 -0
  13. package/{src/images/enrichImages.ts → dist/chunk-YPK3DAFK.js} +44 -61
  14. package/dist/chunk-YPK3DAFK.js.map +1 -0
  15. package/dist/components.d.ts +65 -0
  16. package/dist/components.js +16 -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 +15 -0
  26. package/dist/images.js.map +1 -0
  27. package/dist/index.d.ts +30 -0
  28. package/dist/index.js +67 -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-CWFZ6GB-.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/index.ts +0 -3
  44. package/src/images/pexels.ts +0 -27
  45. package/src/index.ts +0 -58
  46. package/src/themes.ts +0 -204
  47. package/src/types.ts +0 -30
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/themes.ts","../src/iframeScript.ts","../src/buildHtml.ts"],"sourcesContent":["export interface LandingTheme {\n id: string;\n label: string;\n colors: {\n primary: string;\n \"primary-light\": string;\n \"primary-dark\": string;\n secondary: string;\n accent: string;\n surface: string;\n \"surface-alt\": string;\n \"on-surface\": string;\n \"on-surface-muted\": string;\n \"on-primary\": string;\n };\n}\n\nexport const LANDING_THEMES: LandingTheme[] = [\n {\n id: \"default\",\n label: \"Neutral\",\n colors: {\n primary: \"#18181b\",\n \"primary-light\": \"#3f3f46\",\n \"primary-dark\": \"#09090b\",\n secondary: \"#a1a1aa\",\n accent: \"#18181b\",\n surface: \"#ffffff\",\n \"surface-alt\": \"#fafafa\",\n \"on-surface\": \"#18181b\",\n \"on-surface-muted\": \"#71717a\",\n \"on-primary\": \"#ffffff\",\n },\n },\n {\n id: \"dark\",\n label: \"Dark\",\n colors: {\n primary: \"#e4e4e7\",\n \"primary-light\": \"#f4f4f5\",\n \"primary-dark\": \"#a1a1aa\",\n secondary: \"#71717a\",\n accent: \"#a78bfa\",\n surface: \"#09090b\",\n \"surface-alt\": \"#18181b\",\n \"on-surface\": \"#fafafa\",\n \"on-surface-muted\": \"#a1a1aa\",\n \"on-primary\": \"#09090b\",\n },\n },\n {\n id: \"slate\",\n label: \"Slate\",\n colors: {\n primary: \"#3b82f6\",\n \"primary-light\": \"#60a5fa\",\n \"primary-dark\": \"#2563eb\",\n secondary: \"#64748b\",\n accent: \"#3b82f6\",\n surface: \"#ffffff\",\n \"surface-alt\": \"#f8fafc\",\n \"on-surface\": \"#0f172a\",\n \"on-surface-muted\": \"#64748b\",\n \"on-primary\": \"#ffffff\",\n },\n },\n {\n id: \"midnight\",\n label: \"Midnight\",\n colors: {\n primary: \"#6366f1\",\n \"primary-light\": \"#818cf8\",\n \"primary-dark\": \"#4f46e5\",\n secondary: \"#94a3b8\",\n accent: \"#a78bfa\",\n surface: \"#0f172a\",\n \"surface-alt\": \"#1e293b\",\n \"on-surface\": \"#e2e8f0\",\n \"on-surface-muted\": \"#94a3b8\",\n \"on-primary\": \"#ffffff\",\n },\n },\n {\n id: \"warm\",\n label: \"Warm\",\n colors: {\n primary: \"#b45309\",\n \"primary-light\": \"#d97706\",\n \"primary-dark\": \"#92400e\",\n secondary: \"#78716c\",\n accent: \"#b45309\",\n surface: \"#fafaf9\",\n \"surface-alt\": \"#f5f5f4\",\n \"on-surface\": \"#1c1917\",\n \"on-surface-muted\": \"#78716c\",\n \"on-primary\": \"#ffffff\",\n },\n },\n];\n\nexport interface CustomColors {\n primary: string;\n secondary?: string;\n accent?: string;\n surface?: string;\n}\n\nfunction parseHex(hex: string) {\n return {\n r: parseInt(hex.slice(1, 3), 16),\n g: parseInt(hex.slice(3, 5), 16),\n b: parseInt(hex.slice(5, 7), 16),\n };\n}\n\nfunction toHex(r: number, g: number, b: number) {\n return `#${[r, g, b].map((c) => Math.max(0, Math.min(255, c)).toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nfunction luminance(hex: string) {\n const { r, g, b } = parseHex(hex);\n return (0.299 * r + 0.587 * g + 0.114 * b) / 255;\n}\n\nfunction lighten(hex: string, amount = 40) {\n const { r, g, b } = parseHex(hex);\n return toHex(r + amount, g + amount, b + amount);\n}\n\nfunction darken(hex: string, amount = 40) {\n const { r, g, b } = parseHex(hex);\n return toHex(r - amount, g - amount, b - amount);\n}\n\nexport function buildCustomTheme(colors: CustomColors): LandingTheme {\n const { primary, secondary = \"#f59e0b\", accent = \"#06b6d4\", surface = \"#ffffff\" } = colors;\n\n const onPrimary = luminance(primary) > 0.5 ? \"#111827\" : \"#ffffff\";\n const surfaceLum = luminance(surface);\n const isDarkSurface = surfaceLum < 0.5;\n\n return {\n id: \"custom\",\n label: \"Custom\",\n colors: {\n primary,\n \"primary-light\": lighten(primary),\n \"primary-dark\": darken(primary),\n secondary,\n accent,\n surface,\n \"surface-alt\": isDarkSurface ? lighten(surface, 20) : darken(surface, 5),\n \"on-surface\": isDarkSurface ? \"#f1f5f9\" : \"#111827\",\n \"on-surface-muted\": isDarkSurface ? \"#94a3b8\" : \"#6b7280\",\n \"on-primary\": onPrimary,\n },\n };\n}\n\nexport function buildCustomThemeCss(colors: CustomColors): string {\n const theme = buildCustomTheme(colors);\n return `[data-theme=\"custom\"] {\\n${buildCssVars(theme.colors)}\\n}`;\n}\n\n/** CSS custom properties for a theme */\nfunction buildCssVars(colors: LandingTheme[\"colors\"]): string {\n return Object.entries(colors)\n .map(([k, v]) => ` --color-${k}: ${v};`)\n .join(\"\\n\");\n}\n\n/** Build the tailwind.config JS object string for TW v3 CDN */\nfunction buildTailwindConfig(): string {\n const colorEntries = Object.keys(LANDING_THEMES[0].colors)\n .map((k) => ` '${k}': 'var(--color-${k})'`)\n .join(\",\\n\");\n\n return `{\n theme: {\n extend: {\n colors: {\n${colorEntries}\n }\n }\n }\n }`;\n}\n\nexport function buildThemeCss(): { css: string; tailwindConfig: string } {\n const defaultTheme = LANDING_THEMES[0];\n\n const overrides = LANDING_THEMES.slice(1)\n .map((t) => `[data-theme=\"${t.id}\"] {\\n${buildCssVars(t.colors)}\\n}`)\n .join(\"\\n\\n\");\n\n const css = `:root {\\n${buildCssVars(defaultTheme.colors)}\\n}\\n\\n${overrides}`;\n return { css, tailwindConfig: buildTailwindConfig() };\n}\n\nexport function buildSingleThemeCss(themeId: string): { css: string; tailwindConfig: string } {\n const theme = LANDING_THEMES.find((t) => t.id === themeId) || LANDING_THEMES[0];\n const css = `:root {\\n${buildCssVars(theme.colors)}\\n}`;\n return { css, tailwindConfig: buildTailwindConfig() };\n}\n","/**\n * JavaScript injected into the landing v3 iframe.\n * Handles hover highlights, click selection, contentEditable text editing,\n * postMessage communication with the parent editor,\n * and incremental section injection from parent.\n */\nexport function getIframeScript(): string {\n return `\n(function() {\n let hoveredEl = null;\n let selectedEl = null;\n const OUTLINE_HOVER = '2px solid #3B82F6';\n const OUTLINE_SELECTED = '2px solid #8B5CF6';\n\n function getSectionId(el) {\n let node = el;\n while (node && node !== document.body) {\n if (node.dataset && node.dataset.sectionId) {\n return node.dataset.sectionId;\n }\n node = node.parentElement;\n }\n return null;\n }\n\n function getSectionElement(sectionId) {\n return document.querySelector('[data-section-id=\"' + sectionId + '\"]');\n }\n\n function getElementPath(el) {\n const parts = [];\n let node = el;\n while (node && node !== document.body) {\n let tag = node.tagName.toLowerCase();\n if (node.id) { tag += '#' + node.id; }\n const siblings = node.parentElement ? Array.from(node.parentElement.children).filter(function(c) { return c.tagName === node.tagName; }) : [];\n if (siblings.length > 1) { tag += ':nth(' + siblings.indexOf(node) + ')'; }\n parts.unshift(tag);\n node = node.parentElement;\n }\n return parts.join(' > ');\n }\n\n function isTextElement(el) {\n var textTags = ['H1','H2','H3','H4','H5','H6','P','SPAN','LI','A','BLOCKQUOTE','LABEL','TD','TH','FIGCAPTION','BUTTON'];\n return textTags.indexOf(el.tagName) !== -1;\n }\n\n // Hover\n document.addEventListener('mouseover', function(e) {\n var el = e.target;\n if (el === document.body || el === document.documentElement) return;\n if (el === selectedEl) return;\n if (hoveredEl && hoveredEl !== selectedEl) {\n hoveredEl.style.outline = '';\n hoveredEl.style.outlineOffset = '';\n }\n hoveredEl = el;\n if (el !== selectedEl) {\n el.style.outline = OUTLINE_HOVER;\n el.style.outlineOffset = '-2px';\n }\n });\n\n document.addEventListener('mouseout', function(e) {\n if (hoveredEl && hoveredEl !== selectedEl) {\n hoveredEl.style.outline = '';\n hoveredEl.style.outlineOffset = '';\n }\n hoveredEl = null;\n });\n\n // Click — select element\n document.addEventListener('click', function(e) {\n e.preventDefault();\n e.stopPropagation();\n var el = e.target;\n\n // Deselect previous\n if (selectedEl) {\n selectedEl.style.outline = '';\n selectedEl.style.outlineOffset = '';\n }\n\n if (selectedEl === el) {\n selectedEl = null;\n window.parent.postMessage({ type: 'element-deselected' }, '*');\n return;\n }\n\n selectedEl = el;\n\n // Clear hover styles BEFORE capturing openTag (so it matches source HTML)\n el.style.outline = '';\n el.style.outlineOffset = '';\n var openTag = el.outerHTML.substring(0, el.outerHTML.indexOf('>') + 1).substring(0, 120);\n\n el.style.outline = OUTLINE_SELECTED;\n el.style.outlineOffset = '-2px';\n\n var rect = el.getBoundingClientRect();\n var attrs = {};\n if (el.tagName === 'IMG') {\n attrs = { src: el.getAttribute('src') || '', alt: el.getAttribute('alt') || '' };\n }\n if (el.tagName === 'A') {\n attrs = { href: el.getAttribute('href') || '', target: el.getAttribute('target') || '' };\n }\n\n window.parent.postMessage({\n type: 'element-selected',\n sectionId: getSectionId(el),\n tagName: el.tagName,\n rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },\n text: (el.textContent || '').substring(0, 200),\n openTag: openTag,\n elementPath: getElementPath(el),\n isSectionRoot: el.dataset && el.dataset.sectionId ? true : false,\n attrs: attrs,\n }, '*');\n }, true);\n\n // Double-click — contentEditable for text\n document.addEventListener('dblclick', function(e) {\n e.preventDefault();\n e.stopPropagation();\n var el = e.target;\n if (!isTextElement(el)) return;\n\n el.contentEditable = 'true';\n el.focus();\n el.style.outline = '2px dashed #F59E0B';\n el.style.outlineOffset = '-2px';\n\n function onBlur() {\n el.contentEditable = 'false';\n el.style.outline = '';\n el.style.outlineOffset = '';\n el.removeEventListener('blur', onBlur);\n el.removeEventListener('keydown', onKeydown);\n\n var sid = getSectionId(el);\n var sectionEl = sid ? getSectionElement(sid) : null;\n window.parent.postMessage({\n type: 'text-edited',\n sectionId: sid,\n elementPath: getElementPath(el),\n newText: el.innerHTML,\n sectionHtml: sectionEl ? sectionEl.innerHTML : null,\n }, '*');\n\n selectedEl = null;\n }\n\n function onKeydown(ev) {\n if (ev.key === 'Escape') {\n el.blur();\n }\n }\n\n el.addEventListener('blur', onBlur);\n el.addEventListener('keydown', onKeydown);\n }, true);\n\n // Listen for messages FROM parent (incremental section injection)\n window.addEventListener('message', function(e) {\n var msg = e.data;\n if (!msg || !msg.action) return;\n\n if (msg.action === 'add-section') {\n var wrapper = document.createElement('div');\n wrapper.setAttribute('data-section-id', msg.id);\n wrapper.innerHTML = msg.html;\n wrapper.style.animation = 'fadeInUp 0.4s ease-out';\n document.body.appendChild(wrapper);\n wrapper.scrollIntoView({ behavior: 'smooth', block: 'end' });\n }\n\n if (msg.action === 'update-section') {\n var el = getSectionElement(msg.id);\n if (el && typeof window.morphdom === 'function') {\n var tmp = document.createElement('div');\n tmp.innerHTML = msg.html;\n window.morphdom(el, tmp, {\n childrenOnly: true,\n onBeforeElUpdated: function(fromEl, toEl) {\n if (fromEl.isEqualNode(toEl)) return false;\n return true;\n }\n });\n } else if (el) {\n el.innerHTML = msg.html;\n }\n }\n\n if (msg.action === 'remove-section') {\n var el = getSectionElement(msg.id);\n if (el) { el.remove(); }\n }\n\n if (msg.action === 'reorder-sections') {\n // msg.order = [id1, id2, id3, ...]\n var order = msg.order;\n for (var i = 0; i < order.length; i++) {\n var el = getSectionElement(order[i]);\n if (el) { document.body.appendChild(el); }\n }\n }\n\n if (msg.action === 'update-attribute') {\n var sectionEl = getSectionElement(msg.sectionId);\n if (sectionEl) {\n var target = null;\n if (msg.elementPath) {\n // Find element by matching path\n var allEls = sectionEl.querySelectorAll(msg.tagName || '*');\n for (var i = 0; i < allEls.length; i++) {\n if (getElementPath(allEls[i]) === msg.elementPath) {\n target = allEls[i];\n break;\n }\n }\n }\n if (target) {\n target.setAttribute(msg.attr, msg.value);\n window.parent.postMessage({\n type: 'section-html-updated',\n sectionId: msg.sectionId,\n sectionHtml: sectionEl.innerHTML,\n }, '*');\n }\n }\n }\n\n if (msg.action === 'set-theme') {\n if (msg.theme && msg.theme !== 'default') {\n document.documentElement.setAttribute('data-theme', msg.theme);\n } else {\n document.documentElement.removeAttribute('data-theme');\n }\n }\n\n if (msg.action === 'set-custom-css') {\n var customStyle = document.getElementById('custom-theme-css');\n if (!customStyle) {\n customStyle = document.createElement('style');\n customStyle.id = 'custom-theme-css';\n document.head.appendChild(customStyle);\n }\n customStyle.textContent = msg.css || '';\n }\n\n if (msg.action === 'scroll-to-section') {\n var el = getSectionElement(msg.id);\n if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }\n }\n\n if (msg.action === 'full-rewrite') {\n // Fallback: rewrite everything\n document.body.innerHTML = msg.html;\n }\n });\n\n // Inject animation keyframe\n var style = document.createElement('style');\n style.textContent = '@keyframes fadeInUp { from { opacity:0; transform:translateY(20px); } to { opacity:1; transform:translateY(0); } }';\n document.head.appendChild(style);\n\n // Notify parent we're ready\n window.parent.postMessage({ type: 'ready' }, '*');\n})();\n`;\n}\n","import type { Section3 } from \"./types\";\nimport { getIframeScript } from \"./iframeScript\";\nimport { buildThemeCss, buildSingleThemeCss, buildCustomTheme, type CustomColors } from \"./themes\";\n\n/**\n * Build the full HTML for the iframe preview (with editing script).\n */\nexport function buildPreviewHtml(sections: Section3[], theme?: string): string {\n const sorted = [...sections].sort((a, b) => a.order - b.order);\n const body = sorted\n .map((s) => `<div data-section-id=\"${s.id}\">${s.html}</div>`)\n .join(\"\\n\");\n\n const dataTheme = theme && theme !== \"default\" ? ` data-theme=\"${theme}\"` : \"\";\n const { css, tailwindConfig } = buildThemeCss();\n\n return `<!DOCTYPE html>\n<html lang=\"es\"${dataTheme}>\n<head>\n<meta charset=\"UTF-8\"/>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\n<script src=\"https://cdn.tailwindcss.com\"></script>\n<script src=\"https://unpkg.com/morphdom@2.7.4/dist/morphdom-umd.min.js\"></script>\n<script>tailwind.config = ${tailwindConfig}</script>\n<style>\n${css}\n*{margin:0;padding:0;box-sizing:border-box}\nhtml{scroll-behavior:smooth}\nbody{font-family:system-ui,-apple-system,sans-serif;background-color:var(--color-surface);color:var(--color-on-surface)}\nimg{max-width:100%}\n[contenteditable=\"true\"]{cursor:text}\n</style>\n</head>\n<body class=\"bg-surface text-on-surface\">\n${body}\n<script>${getIframeScript()}</script>\n</body>\n</html>`;\n}\n\n/**\n * Build the deploy HTML (no editing script, clean output).\n */\nexport function buildDeployHtml(sections: Section3[], theme?: string, customColors?: CustomColors): string {\n const sorted = [...sections].sort((a, b) => a.order - b.order);\n const body = sorted.map((s) => s.html).join(\"\\n\");\n\n const isCustom = theme === \"custom\" && customColors;\n const dataTheme = theme && theme !== \"default\" && !isCustom ? ` data-theme=\"${theme}\"` : \"\";\n\n // For custom theme, build CSS from the custom colors directly (no data-theme needed, inject as :root)\n const { css: baseCss, tailwindConfig } = isCustom\n ? (() => {\n const ct = buildCustomTheme(customColors);\n const vars = Object.entries(ct.colors).map(([k, v]) => ` --color-${k}: ${v};`).join(\"\\n\");\n return { css: `:root {\\n${vars}\\n}`, tailwindConfig: buildSingleThemeCss(\"default\").tailwindConfig };\n })()\n : buildSingleThemeCss(theme || \"default\");\n\n return `<!DOCTYPE html>\n<html lang=\"es\"${dataTheme}>\n<head>\n<meta charset=\"UTF-8\"/>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\n<title>Landing Page</title>\n<script src=\"https://cdn.tailwindcss.com\"></script>\n<script>tailwind.config = ${tailwindConfig}</script>\n<style>\n${baseCss}\n*{margin:0;padding:0;box-sizing:border-box}\nhtml{scroll-behavior:smooth}\nbody{font-family:system-ui,-apple-system,sans-serif;background-color:var(--color-surface);color:var(--color-on-surface)}\nsection > * {max-width:80rem;margin-left:auto;margin-right:auto}\n</style>\n</head>\n<body class=\"bg-surface text-on-surface\">\n${body}\n</body>\n</html>`;\n}\n"],"mappings":"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","names":[]}
@@ -1,13 +1,28 @@
1
- import { searchImage } from "./pexels";
2
- import { generateImage } from "./dalleImages";
3
-
4
- interface ImageMatch {
5
- query: string;
6
- searchStr: string;
7
- replaceStr: string;
1
+ // src/images/pexels.ts
2
+ async function searchImage(query, apiKey) {
3
+ const key = apiKey || process.env.PEXELS_API_KEY;
4
+ if (!key) return null;
5
+ try {
6
+ const res = await fetch(
7
+ `https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=1&orientation=landscape`,
8
+ { headers: { Authorization: key } }
9
+ );
10
+ if (!res.ok) return null;
11
+ const data = await res.json();
12
+ const photo = data.photos?.[0];
13
+ if (!photo) return null;
14
+ return {
15
+ url: photo.src.large,
16
+ photographer: photo.photographer,
17
+ alt: photo.alt || query
18
+ };
19
+ } catch {
20
+ return null;
21
+ }
8
22
  }
9
23
 
10
- const FAKE_DOMAINS = [
24
+ // src/images/enrichImages.ts
25
+ var FAKE_DOMAINS = [
11
26
  "images.unsplash.com",
12
27
  "unsplash.com",
13
28
  "via.placeholder.com",
@@ -22,22 +37,13 @@ const FAKE_DOMAINS = [
22
37
  "fakeimg.pl",
23
38
  "example.com",
24
39
  "img.freepik.com",
25
- "cdn.pixabay.com",
40
+ "cdn.pixabay.com"
26
41
  ];
27
-
28
- /**
29
- * Find all images in HTML that need Pexels enrichment.
30
- * Two strategies:
31
- * 1. data-image-query="..." — AI followed instructions
32
- * 2. <img src="fake-url" — detect fake domains, use alt/class/nearby text as query
33
- */
34
- export function findImageSlots(html: string): ImageMatch[] {
35
- const matches: ImageMatch[] = [];
36
- const seen = new Set<string>();
37
-
38
- // 1. data-image-query="..."
42
+ function findImageSlots(html) {
43
+ const matches = [];
44
+ const seen = /* @__PURE__ */ new Set();
39
45
  const diqRegex = /data-image-query="([^"]+)"/g;
40
- let m: RegExpExecArray | null;
46
+ let m;
41
47
  while ((m = diqRegex.exec(html)) !== null) {
42
48
  const query = m[1];
43
49
  if (seen.has(query)) continue;
@@ -45,21 +51,16 @@ export function findImageSlots(html: string): ImageMatch[] {
45
51
  matches.push({
46
52
  query,
47
53
  searchStr: `data-image-query="${query}"`,
48
- replaceStr: `src="{url}" data-enriched="true"`,
54
+ replaceStr: `src="{url}" data-enriched="true"`
49
55
  });
50
56
  }
51
-
52
- // 2. <img with fake/non-existent src URLs
53
57
  const imgRegex = /<img\s[^>]*src="(https?:\/\/[^"]+)"[^>]*>/gi;
54
58
  while ((m = imgRegex.exec(html)) !== null) {
55
59
  const fullTag = m[0];
56
60
  const srcUrl = m[1];
57
-
58
61
  if (fullTag.includes("data-enriched")) continue;
59
62
  if (srcUrl.includes("pexels.com")) continue;
60
63
  if (seen.has(srcUrl)) continue;
61
-
62
- // Check if domain is fake
63
64
  let isFake = false;
64
65
  try {
65
66
  const domain = new URL(srcUrl).hostname;
@@ -68,68 +69,50 @@ export function findImageSlots(html: string): ImageMatch[] {
68
69
  isFake = true;
69
70
  }
70
71
  if (!isFake) continue;
71
-
72
- // Extract query: try alt, then class context, then URL path words
73
72
  const altMatch = fullTag.match(/alt="([^"]*?)"/);
74
73
  let query = altMatch?.[1]?.trim() || "";
75
-
76
74
  if (!query) {
77
- // Try to extract meaningful words from the URL path
78
75
  try {
79
76
  const path = new URL(srcUrl).pathname;
80
- const words = path
81
- .replace(/[^a-zA-Z]/g, " ")
82
- .split(/\s+/)
83
- .filter((w) => w.length > 2)
84
- .slice(0, 4)
85
- .join(" ");
77
+ const words = path.replace(/[^a-zA-Z]/g, " ").split(/\s+/).filter((w) => w.length > 2).slice(0, 4).join(" ");
86
78
  if (words.length > 3) query = words;
87
- } catch { /* ignore */ }
79
+ } catch {
80
+ }
88
81
  }
89
-
90
82
  if (!query) query = "professional website hero image";
91
-
92
83
  seen.add(srcUrl);
93
84
  matches.push({
94
85
  query,
95
86
  searchStr: `src="${srcUrl}"`,
96
- replaceStr: `src="{url}" data-enriched="true"`,
87
+ replaceStr: `src="{url}" data-enriched="true"`
97
88
  });
98
89
  }
99
-
100
90
  return matches;
101
91
  }
102
-
103
- /**
104
- * Enrich all images in an HTML string with Pexels photos.
105
- */
106
- export async function enrichImages(html: string, pexelsApiKey?: string, openaiApiKey?: string): Promise<string> {
92
+ async function enrichImages(html, pexelsApiKey, openaiApiKey) {
107
93
  const slots = findImageSlots(html);
108
94
  if (slots.length === 0) return html;
109
-
110
95
  let result = html;
111
96
  const promises = slots.map(async (slot) => {
112
- let url: string | null = null;
113
- if (openaiApiKey) {
114
- url = await generateImage(slot.query, openaiApiKey).catch(() => null);
115
- }
116
- if (!url) {
117
- const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
118
- url = img?.url || null;
119
- }
97
+ let url = null;
98
+ const img = await searchImage(slot.query, pexelsApiKey).catch(() => null);
99
+ url = img?.url || null;
120
100
  url ??= `https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(slot.query.slice(0, 30))}`;
121
101
  const replacement = slot.replaceStr.replace("{url}", url);
122
102
  result = result.replaceAll(slot.searchStr, replacement);
123
103
  });
124
-
125
104
  await Promise.allSettled(promises);
126
-
127
- // Catch any remaining <img> tags without src (AI didn't follow instructions)
128
105
  result = result.replace(/<img\s(?![^>]*\bsrc=)([^>]*?)>/gi, (_match, attrs) => {
129
106
  const altMatch = attrs.match(/alt="([^"]*?)"/);
130
107
  const query = altMatch?.[1] || "professional image";
131
108
  return `<img src="https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}" ${attrs}>`;
132
109
  });
133
-
134
110
  return result;
135
111
  }
112
+
113
+ export {
114
+ searchImage,
115
+ findImageSlots,
116
+ enrichImages
117
+ };
118
+ //# sourceMappingURL=chunk-YPK3DAFK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/images/pexels.ts","../src/images/enrichImages.ts"],"sourcesContent":["export interface PexelsResult {\n url: string;\n photographer: string;\n alt: string;\n}\n\nexport async function searchImage(query: string, apiKey?: string): Promise<PexelsResult | null> {\n const key = apiKey || process.env.PEXELS_API_KEY;\n if (!key) return null;\n try {\n const res = await fetch(\n `https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=1&orientation=landscape`,\n { headers: { Authorization: key } }\n );\n if (!res.ok) return null;\n const data = await res.json();\n const photo = data.photos?.[0];\n if (!photo) return null;\n return {\n url: photo.src.large,\n photographer: photo.photographer,\n alt: photo.alt || query,\n };\n } catch {\n return null;\n }\n}\n","import { searchImage } from \"./pexels\";\nimport { generateImage } from \"./dalleImages\";\n\ninterface ImageMatch {\n query: string;\n searchStr: string;\n replaceStr: string;\n}\n\nconst FAKE_DOMAINS = [\n \"images.unsplash.com\",\n \"unsplash.com\",\n \"via.placeholder.com\",\n \"placeholder.com\",\n \"placehold.co\",\n \"placehold.it\",\n \"placekitten.com\",\n \"picsum.photos\",\n \"loremflickr.com\",\n \"source.unsplash.com\",\n \"dummyimage.com\",\n \"fakeimg.pl\",\n \"example.com\",\n \"img.freepik.com\",\n \"cdn.pixabay.com\",\n];\n\n/**\n * Find all images in HTML that need Pexels enrichment.\n * Two strategies:\n * 1. data-image-query=\"...\" — AI followed instructions\n * 2. <img src=\"fake-url\" — detect fake domains, use alt/class/nearby text as query\n */\nexport function findImageSlots(html: string): ImageMatch[] {\n const matches: ImageMatch[] = [];\n const seen = new Set<string>();\n\n // 1. data-image-query=\"...\"\n const diqRegex = /data-image-query=\"([^\"]+)\"/g;\n let m: RegExpExecArray | null;\n while ((m = diqRegex.exec(html)) !== null) {\n const query = m[1];\n if (seen.has(query)) continue;\n seen.add(query);\n matches.push({\n query,\n searchStr: `data-image-query=\"${query}\"`,\n replaceStr: `src=\"{url}\" data-enriched=\"true\"`,\n });\n }\n\n // 2. <img with fake/non-existent src URLs\n const imgRegex = /<img\\s[^>]*src=\"(https?:\\/\\/[^\"]+)\"[^>]*>/gi;\n while ((m = imgRegex.exec(html)) !== null) {\n const fullTag = m[0];\n const srcUrl = m[1];\n\n if (fullTag.includes(\"data-enriched\")) continue;\n if (srcUrl.includes(\"pexels.com\")) continue;\n if (seen.has(srcUrl)) continue;\n\n // Check if domain is fake\n let isFake = false;\n try {\n const domain = new URL(srcUrl).hostname;\n isFake = FAKE_DOMAINS.some((d) => domain.includes(d));\n } catch {\n isFake = true;\n }\n if (!isFake) continue;\n\n // Extract query: try alt, then class context, then URL path words\n const altMatch = fullTag.match(/alt=\"([^\"]*?)\"/);\n let query = altMatch?.[1]?.trim() || \"\";\n\n if (!query) {\n // Try to extract meaningful words from the URL path\n try {\n const path = new URL(srcUrl).pathname;\n const words = path\n .replace(/[^a-zA-Z]/g, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 2)\n .slice(0, 4)\n .join(\" \");\n if (words.length > 3) query = words;\n } catch { /* ignore */ }\n }\n\n if (!query) query = \"professional website hero image\";\n\n seen.add(srcUrl);\n matches.push({\n query,\n searchStr: `src=\"${srcUrl}\"`,\n replaceStr: `src=\"{url}\" data-enriched=\"true\"`,\n });\n }\n\n return matches;\n}\n\n/**\n * Enrich all images in an HTML string with Pexels photos.\n */\nexport async function enrichImages(html: string, pexelsApiKey?: string, openaiApiKey?: string): Promise<string> {\n const slots = findImageSlots(html);\n if (slots.length === 0) return html;\n\n let result = html;\n const promises = slots.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 const replacement = slot.replaceStr.replace(\"{url}\", url);\n result = result.replaceAll(slot.searchStr, replacement);\n });\n\n await Promise.allSettled(promises);\n\n // Catch any remaining <img> tags without src (AI didn't follow instructions)\n result = result.replace(/<img\\s(?![^>]*\\bsrc=)([^>]*?)>/gi, (_match, attrs) => {\n const altMatch = attrs.match(/alt=\"([^\"]*?)\"/);\n const query = altMatch?.[1] || \"professional image\";\n return `<img src=\"https://placehold.co/800x500/1f2937/9ca3af?text=${encodeURIComponent(query.slice(0, 30))}\" ${attrs}>`;\n });\n\n return result;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,65 @@
1
+ import React from 'react';
2
+ import { S as Section3, I as IframeMessage } from './types-Flpl4wDs.js';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { C as CustomColors } from './themes-CWFZ6GB-.js';
5
+
6
+ interface CanvasHandle {
7
+ scrollToSection: (id: string) => void;
8
+ postMessage: (msg: Record<string, unknown>) => void;
9
+ }
10
+ interface CanvasProps {
11
+ sections: Section3[];
12
+ theme?: string;
13
+ onMessage: (msg: IframeMessage) => void;
14
+ iframeRectRef: React.MutableRefObject<DOMRect | null>;
15
+ }
16
+ declare const Canvas: React.ForwardRefExoticComponent<CanvasProps & React.RefAttributes<CanvasHandle>>;
17
+
18
+ interface SectionListProps {
19
+ sections: Section3[];
20
+ selectedSectionId: string | null;
21
+ theme: string;
22
+ customColors?: CustomColors;
23
+ onThemeChange: (themeId: string) => void;
24
+ onCustomColorChange?: (colors: Partial<CustomColors>) => void;
25
+ onSelect: (id: string) => void;
26
+ onOpenCode: (id: string) => void;
27
+ onReorder: (fromIndex: number, toIndex: number) => void;
28
+ onDelete: (id: string) => void;
29
+ onRename: (id: string, label: string) => void;
30
+ onAdd: () => void;
31
+ }
32
+ declare function SectionList({ sections, selectedSectionId, theme, customColors, onThemeChange, onCustomColorChange, onSelect, onOpenCode, onReorder, onDelete, onRename, onAdd, }: SectionListProps): react_jsx_runtime.JSX.Element;
33
+
34
+ interface FloatingToolbarProps {
35
+ selection: IframeMessage | null;
36
+ iframeRect: DOMRect | null;
37
+ onRefine: (instruction: string, referenceImage?: string) => void;
38
+ onMoveUp: () => void;
39
+ onMoveDown: () => void;
40
+ onDelete: () => void;
41
+ onClose: () => void;
42
+ onViewCode: () => void;
43
+ onUpdateAttribute?: (sectionId: string, elementPath: string, attr: string, value: string) => void;
44
+ isRefining: boolean;
45
+ }
46
+ declare function FloatingToolbar({ selection, iframeRect, onRefine, onMoveUp, onMoveDown, onDelete, onClose, onViewCode, onUpdateAttribute, isRefining, }: FloatingToolbarProps): react_jsx_runtime.JSX.Element | null;
47
+
48
+ interface CodeEditorProps {
49
+ code: string;
50
+ label: string;
51
+ scrollToText?: string;
52
+ onSave: (code: string) => void;
53
+ onClose: () => void;
54
+ }
55
+ declare function CodeEditor({ code, label, scrollToText, onSave, onClose }: CodeEditorProps): react_jsx_runtime.JSX.Element;
56
+
57
+ type Viewport = "desktop" | "tablet" | "mobile";
58
+ declare function ViewportToggle({ value, onChange, activeClass, inactiveClass, }: {
59
+ value: Viewport;
60
+ onChange: (v: Viewport) => void;
61
+ activeClass?: string;
62
+ inactiveClass?: string;
63
+ }): react_jsx_runtime.JSX.Element;
64
+
65
+ export { Canvas, type CanvasHandle, CodeEditor, FloatingToolbar, SectionList, type Viewport, ViewportToggle };
@@ -0,0 +1,16 @@
1
+ import {
2
+ Canvas,
3
+ CodeEditor,
4
+ FloatingToolbar,
5
+ SectionList,
6
+ ViewportToggle
7
+ } from "./chunk-464CGCFJ.js";
8
+ import "./chunk-UGIQBLG5.js";
9
+ export {
10
+ Canvas,
11
+ CodeEditor,
12
+ FloatingToolbar,
13
+ SectionList,
14
+ ViewportToggle
15
+ };
16
+ //# sourceMappingURL=components.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,39 @@
1
+ import { S as Section3 } from './types-Flpl4wDs.js';
2
+ import { C as CustomColors } from './themes-CWFZ6GB-.js';
3
+
4
+ interface DeployToS3Options {
5
+ /** The sections to deploy */
6
+ sections: Section3[];
7
+ /** Theme ID */
8
+ theme?: string;
9
+ /** Custom colors (when theme is "custom") */
10
+ customColors?: CustomColors;
11
+ /** S3-compatible upload function. Receives the HTML string, returns the URL */
12
+ upload: (html: string) => Promise<string>;
13
+ }
14
+ /**
15
+ * Deploy a landing page to any S3-compatible storage.
16
+ * The consumer provides their own upload function.
17
+ */
18
+ declare function deployToS3(options: DeployToS3Options): Promise<string>;
19
+ interface DeployToEasyBitsOptions {
20
+ /** EasyBits API key */
21
+ apiKey: string;
22
+ /** Website slug (e.g. "my-landing" → my-landing.easybits.cloud) */
23
+ slug: string;
24
+ /** The sections to deploy */
25
+ sections: Section3[];
26
+ /** Theme ID */
27
+ theme?: string;
28
+ /** Custom colors (when theme is "custom") */
29
+ customColors?: CustomColors;
30
+ /** EasyBits API base URL (default: https://easybits.cloud) */
31
+ baseUrl?: string;
32
+ }
33
+ /**
34
+ * Deploy a landing page to EasyBits hosting (slug.easybits.cloud).
35
+ * Uses the EasyBits API to create/update a website.
36
+ */
37
+ declare function deployToEasyBits(options: DeployToEasyBitsOptions): Promise<string>;
38
+
39
+ export { type DeployToEasyBitsOptions, type DeployToS3Options, deployToEasyBits, deployToS3 };
package/dist/deploy.js ADDED
@@ -0,0 +1,10 @@
1
+ import {
2
+ deployToEasyBits,
3
+ deployToS3
4
+ } from "./chunk-24P7DHB7.js";
5
+ import "./chunk-UGIQBLG5.js";
6
+ export {
7
+ deployToEasyBits,
8
+ deployToS3
9
+ };
10
+ //# sourceMappingURL=deploy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,41 @@
1
+ import { S as Section3 } from './types-Flpl4wDs.js';
2
+
3
+ declare 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) \u2014 make it specific to the prompt\n\nIMAGES \u2014 CRITICAL:\n- Use <img data-image-query=\"english search query\" alt=\"description\" class=\"...\"/>\n- NEVER include a src attribute \u2014 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 \u2014 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 \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.\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 \u2014 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 \u2014 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 \u2192 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";
4
+ declare const PROMPT_SUFFIX = "\n\nOUTPUT FORMAT: NDJSON \u2014 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 \u2014 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.";
5
+ /**
6
+ * Extract complete JSON objects from accumulated text using brace-depth tracking.
7
+ */
8
+ declare function extractJsonObjects(text: string): [any[], string];
9
+ interface GenerateOptions {
10
+ /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */
11
+ anthropicApiKey?: string;
12
+ /** OpenAI API key. If provided, uses GPT-4o instead of Claude */
13
+ openaiApiKey?: string;
14
+ /** Landing page description prompt */
15
+ prompt: string;
16
+ /** Reference image (base64 data URI) for vision-based generation */
17
+ referenceImage?: string;
18
+ /** Extra instructions appended to the prompt */
19
+ extraInstructions?: string;
20
+ /** Custom system prompt (overrides default SYSTEM_PROMPT) */
21
+ systemPrompt?: string;
22
+ /** Model ID (default: gpt-4o for OpenAI, claude-sonnet-4-6 for Anthropic) */
23
+ model?: string;
24
+ /** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */
25
+ pexelsApiKey?: string;
26
+ /** Called when a new section is parsed from the stream */
27
+ onSection?: (section: Section3) => void;
28
+ /** Called when a section's images are enriched */
29
+ onImageUpdate?: (sectionId: string, html: string) => void;
30
+ /** Called when generation is complete */
31
+ onDone?: (sections: Section3[]) => void;
32
+ /** Called on error */
33
+ onError?: (error: Error) => void;
34
+ }
35
+ /**
36
+ * Generate a landing page with streaming AI + image enrichment.
37
+ * Returns all generated sections when complete.
38
+ */
39
+ declare function generateLanding(options: GenerateOptions): Promise<Section3[]>;
40
+
41
+ export { type GenerateOptions, PROMPT_SUFFIX, SYSTEM_PROMPT, extractJsonObjects, generateLanding };
@@ -0,0 +1,14 @@
1
+ import {
2
+ PROMPT_SUFFIX,
3
+ SYSTEM_PROMPT,
4
+ extractJsonObjects,
5
+ generateLanding
6
+ } from "./chunk-S7YLW6ZU.js";
7
+ import "./chunk-YPK3DAFK.js";
8
+ export {
9
+ PROMPT_SUFFIX,
10
+ SYSTEM_PROMPT,
11
+ extractJsonObjects,
12
+ generateLanding
13
+ };
14
+ //# sourceMappingURL=generate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,30 @@
1
+ interface PexelsResult {
2
+ url: string;
3
+ photographer: string;
4
+ alt: string;
5
+ }
6
+ declare function searchImage(query: string, apiKey?: string): Promise<PexelsResult | null>;
7
+
8
+ interface ImageMatch {
9
+ query: string;
10
+ searchStr: string;
11
+ replaceStr: string;
12
+ }
13
+ /**
14
+ * Find all images in HTML that need Pexels enrichment.
15
+ * Two strategies:
16
+ * 1. data-image-query="..." — AI followed instructions
17
+ * 2. <img src="fake-url" — detect fake domains, use alt/class/nearby text as query
18
+ */
19
+ declare function findImageSlots(html: string): ImageMatch[];
20
+ /**
21
+ * Enrich all images in an HTML string with Pexels photos.
22
+ */
23
+ declare function enrichImages(html: string, pexelsApiKey?: string, openaiApiKey?: string): Promise<string>;
24
+
25
+ /**
26
+ * Generate an image using DALL-E 3 API.
27
+ */
28
+ declare function generateImage(query: string, openaiApiKey: string): Promise<string>;
29
+
30
+ export { type PexelsResult, enrichImages, findImageSlots, generateImage, searchImage };
package/dist/images.js ADDED
@@ -0,0 +1,15 @@
1
+ import {
2
+ generateImage
3
+ } from "./chunk-LPI2QUCL.js";
4
+ import {
5
+ enrichImages,
6
+ findImageSlots,
7
+ searchImage
8
+ } from "./chunk-YPK3DAFK.js";
9
+ export {
10
+ enrichImages,
11
+ findImageSlots,
12
+ generateImage,
13
+ searchImage
14
+ };
15
+ //# sourceMappingURL=images.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,30 @@
1
+ import { S as Section3 } from './types-Flpl4wDs.js';
2
+ export { I as IframeMessage } from './types-Flpl4wDs.js';
3
+ import { C as CustomColors } from './themes-CWFZ6GB-.js';
4
+ export { L as LANDING_THEMES, a as LandingTheme, b as buildCustomTheme, c as buildCustomThemeCss, d as buildSingleThemeCss, e as buildThemeCss } from './themes-CWFZ6GB-.js';
5
+ export { GenerateOptions, PROMPT_SUFFIX, SYSTEM_PROMPT, extractJsonObjects, generateLanding } from './generate.js';
6
+ export { REFINE_SYSTEM, RefineOptions, refineLanding } from './refine.js';
7
+ export { DeployToEasyBitsOptions, DeployToS3Options, deployToEasyBits, deployToS3 } from './deploy.js';
8
+ export { PexelsResult, enrichImages, findImageSlots, generateImage, searchImage } from './images.js';
9
+ export { Canvas, CanvasHandle, CodeEditor, FloatingToolbar, SectionList, Viewport, ViewportToggle } from './components.js';
10
+ import 'react';
11
+ import 'react/jsx-runtime';
12
+
13
+ /**
14
+ * Build the full HTML for the iframe preview (with editing script).
15
+ */
16
+ declare function buildPreviewHtml(sections: Section3[], theme?: string): string;
17
+ /**
18
+ * Build the deploy HTML (no editing script, clean output).
19
+ */
20
+ declare function buildDeployHtml(sections: Section3[], theme?: string, customColors?: CustomColors): string;
21
+
22
+ /**
23
+ * JavaScript injected into the landing v3 iframe.
24
+ * Handles hover highlights, click selection, contentEditable text editing,
25
+ * postMessage communication with the parent editor,
26
+ * and incremental section injection from parent.
27
+ */
28
+ declare function getIframeScript(): string;
29
+
30
+ export { CustomColors, Section3, buildDeployHtml, buildPreviewHtml, getIframeScript };
package/dist/index.js ADDED
@@ -0,0 +1,67 @@
1
+ import {
2
+ Canvas,
3
+ CodeEditor,
4
+ FloatingToolbar,
5
+ SectionList,
6
+ ViewportToggle
7
+ } from "./chunk-464CGCFJ.js";
8
+ import {
9
+ generateImage
10
+ } from "./chunk-LPI2QUCL.js";
11
+ import {
12
+ PROMPT_SUFFIX,
13
+ SYSTEM_PROMPT,
14
+ extractJsonObjects,
15
+ generateLanding
16
+ } from "./chunk-S7YLW6ZU.js";
17
+ import {
18
+ REFINE_SYSTEM,
19
+ refineLanding
20
+ } from "./chunk-CB2LECVT.js";
21
+ import {
22
+ enrichImages,
23
+ findImageSlots,
24
+ searchImage
25
+ } from "./chunk-YPK3DAFK.js";
26
+ import {
27
+ deployToEasyBits,
28
+ deployToS3
29
+ } from "./chunk-24P7DHB7.js";
30
+ import {
31
+ LANDING_THEMES,
32
+ buildCustomTheme,
33
+ buildCustomThemeCss,
34
+ buildDeployHtml,
35
+ buildPreviewHtml,
36
+ buildSingleThemeCss,
37
+ buildThemeCss,
38
+ getIframeScript
39
+ } from "./chunk-UGIQBLG5.js";
40
+ export {
41
+ Canvas,
42
+ CodeEditor,
43
+ FloatingToolbar,
44
+ LANDING_THEMES,
45
+ PROMPT_SUFFIX,
46
+ REFINE_SYSTEM,
47
+ SYSTEM_PROMPT,
48
+ SectionList,
49
+ ViewportToggle,
50
+ buildCustomTheme,
51
+ buildCustomThemeCss,
52
+ buildDeployHtml,
53
+ buildPreviewHtml,
54
+ buildSingleThemeCss,
55
+ buildThemeCss,
56
+ deployToEasyBits,
57
+ deployToS3,
58
+ enrichImages,
59
+ extractJsonObjects,
60
+ findImageSlots,
61
+ generateImage,
62
+ generateLanding,
63
+ getIframeScript,
64
+ refineLanding,
65
+ searchImage
66
+ };
67
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,32 @@
1
+ declare const REFINE_SYSTEM = "You are an expert HTML/Tailwind CSS developer. You receive the current HTML of a landing page section and a user instruction.\n\nRULES:\n- Return ONLY the modified HTML \u2014 no full page, no <html>/<head>/<body> tags\n- Use Tailwind CSS classes (CDN loaded)\n- You may use inline styles for specific adjustments\n- Images: use data-image-query=\"english search query\" for new images\n- Keep all text in its original language unless asked to translate\n- Be creative \u2014 don't just make minimal changes, improve the design\n- Return raw HTML only \u2014 no markdown fences, no explanations\n\nCOLOR SYSTEM \u2014 CRITICAL:\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 colors: NO bg-gray-*, bg-black, bg-white, text-gray-*, text-black, text-white, etc.\n- The ONLY exception: border-gray-200 or border-gray-700 for subtle dividers.\n- CONTRAST RULE: on bg-primary/bg-primary-dark \u2192 text-on-primary. On bg-surface/bg-surface-alt \u2192 text-on-surface/text-on-surface-muted. Never mismatch.\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";
2
+ interface RefineOptions {
3
+ /** Anthropic API key. Falls back to ANTHROPIC_API_KEY env var */
4
+ anthropicApiKey?: string;
5
+ /** OpenAI API key. If provided, uses GPT-4o-mini instead of Claude */
6
+ openaiApiKey?: string;
7
+ /** Current HTML of the section being refined */
8
+ currentHtml: string;
9
+ /** User instruction for refinement */
10
+ instruction: string;
11
+ /** Reference image (base64 data URI) for vision-based refinement */
12
+ referenceImage?: string;
13
+ /** Custom system prompt (overrides default REFINE_SYSTEM) */
14
+ systemPrompt?: string;
15
+ /** Model ID (default: gpt-4o-mini/gpt-4o for OpenAI, claude-haiku/claude-sonnet for Anthropic) */
16
+ model?: string;
17
+ /** Pexels API key for image enrichment. Falls back to PEXELS_API_KEY env var */
18
+ pexelsApiKey?: string;
19
+ /** Called with accumulated HTML as it streams */
20
+ onChunk?: (html: string) => void;
21
+ /** Called when refinement is complete with final enriched HTML */
22
+ onDone?: (html: string) => void;
23
+ /** Called on error */
24
+ onError?: (error: Error) => void;
25
+ }
26
+ /**
27
+ * Refine a landing page section with streaming AI.
28
+ * Returns the final enriched HTML.
29
+ */
30
+ declare function refineLanding(options: RefineOptions): Promise<string>;
31
+
32
+ export { REFINE_SYSTEM, type RefineOptions, refineLanding };