(() => { const OVERLAY_ID = "__kleap_overlay__"; let overlay, label; // The possible states are: // { type: 'inactive' } // { type: 'inspecting', element: ?HTMLElement } // { type: 'selected', element: HTMLElement } let state = { type: "inactive" }; /* ---------- i18n translations ----------------------------------------- */ const translations = { en: { editText: "Edit text", mentionToAI: "Mention to AI", replaceImage: "Replace image", chooseFromLibrary: "Choose from library", changeUrl: "Change URL", generateWithAI: "Generate with AI", uploadImage: "Upload image", imageUrl: "Image URL", cancel: "Cancel", apply: "Apply", generate: "🎨 Generate", generating: "Generating...", enterImageUrl: "Enter image URL...", chooseModel: "Choose Model:", describeImage: "Describe your image:", aiPromptPlaceholder: "A beautiful landscape with mountains and a sunset...", stylePresets: "Style presets (optional):", urlPlaceholder: "https://example.com/image.jpg", generatingWith: "Generating with", mayTakeTime: "This may take 10-30 seconds", }, fr: { editText: "Modifier le texte", mentionToAI: "Mentionner à l'IA", replaceImage: "Remplacer l'image", chooseFromLibrary: "Choisir dans la bibliothèque", changeUrl: "Changer l'URL", generateWithAI: "Générer avec l'IA", uploadImage: "Télécharger une image", imageUrl: "URL de l'image", cancel: "Annuler", apply: "Appliquer", generate: "🎨 Générer", generating: "Génération...", enterImageUrl: "Entrez l'URL de l'image...", chooseModel: "Choisir le modèle :", describeImage: "Décrivez votre image :", aiPromptPlaceholder: "Un beau paysage avec des montagnes et un coucher de soleil...", stylePresets: "Préréglages de style (optionnel) :", urlPlaceholder: "https://exemple.com/image.jpg", generatingWith: "Génération avec", mayTakeTime: "Cela peut prendre 10-30 secondes", }, de: { editText: "Text bearbeiten", mentionToAI: "KI erwähnen", replaceImage: "Bild ersetzen", chooseFromLibrary: "Aus Bibliothek wählen", changeUrl: "URL ändern", generateWithAI: "Mit KI generieren", uploadImage: "Bild hochladen", imageUrl: "Bild-URL", cancel: "Abbrechen", apply: "Anwenden", generate: "🎨 Generieren", generating: "Generierung...", enterImageUrl: "Bild-URL eingeben...", chooseModel: "Modell wählen:", describeImage: "Beschreiben Sie Ihr Bild:", aiPromptPlaceholder: "Eine schöne Landschaft mit Bergen und Sonnenuntergang...", stylePresets: "Stilvorlagen (optional):", urlPlaceholder: "https://beispiel.com/bild.jpg", generatingWith: "Generierung mit", mayTakeTime: "Dies kann 10-30 Sekunden dauern", }, es: { editText: "Editar texto", mentionToAI: "Mencionar a la IA", replaceImage: "Reemplazar imagen", chooseFromLibrary: "Elegir de la biblioteca", changeUrl: "Cambiar URL", generateWithAI: "Generar con IA", uploadImage: "Subir imagen", imageUrl: "URL de imagen", cancel: "Cancelar", apply: "Aplicar", generate: "🎨 Generar", generating: "Generando...", enterImageUrl: "Ingrese URL de imagen...", chooseModel: "Elegir modelo:", describeImage: "Describe tu imagen:", aiPromptPlaceholder: "Un hermoso paisaje con montañas y atardecer...", stylePresets: "Estilos predefinidos (opcional):", urlPlaceholder: "https://ejemplo.com/imagen.jpg", generatingWith: "Generando con", mayTakeTime: "Esto puede tomar 10-30 segundos", }, pt: { editText: "Editar texto", mentionToAI: "Mencionar à IA", replaceImage: "Substituir imagem", chooseFromLibrary: "Escolher da biblioteca", changeUrl: "Alterar URL", generateWithAI: "Gerar com IA", uploadImage: "Enviar imagem", imageUrl: "URL da imagem", cancel: "Cancelar", apply: "Aplicar", generate: "🎨 Gerar", generating: "Gerando...", enterImageUrl: "Digite a URL da imagem...", chooseModel: "Escolher modelo:", describeImage: "Descreva sua imagem:", aiPromptPlaceholder: "Uma bela paisagem com montanhas e pôr do sol...", stylePresets: "Estilos predefinidos (opcional):", urlPlaceholder: "https://exemplo.com/imagem.jpg", generatingWith: "Gerando com", mayTakeTime: "Isso pode levar 10-30 segundos", }, it: { editText: "Modifica testo", mentionToAI: "Menziona all'IA", replaceImage: "Sostituisci immagine", chooseFromLibrary: "Scegli dalla libreria", changeUrl: "Cambia URL", generateWithAI: "Genera con IA", uploadImage: "Carica immagine", imageUrl: "URL immagine", cancel: "Annulla", apply: "Applica", generate: "🎨 Genera", generating: "Generazione...", enterImageUrl: "Inserisci URL immagine...", chooseModel: "Scegli modello:", describeImage: "Descrivi la tua immagine:", aiPromptPlaceholder: "Un bellissimo paesaggio con montagne e tramonto...", stylePresets: "Stili predefiniti (opzionale):", urlPlaceholder: "https://esempio.com/immagine.jpg", generatingWith: "Generazione con", mayTakeTime: "Potrebbe richiedere 10-30 secondi", }, nl: { editText: "Tekst bewerken", mentionToAI: "AI vermelden", replaceImage: "Afbeelding vervangen", chooseFromLibrary: "Kies uit bibliotheek", changeUrl: "URL wijzigen", generateWithAI: "Genereren met AI", uploadImage: "Afbeelding uploaden", imageUrl: "Afbeelding-URL", cancel: "Annuleren", apply: "Toepassen", generate: "🎨 Genereren", generating: "Genereren...", enterImageUrl: "Voer afbeelding-URL in...", chooseModel: "Kies model:", describeImage: "Beschrijf je afbeelding:", aiPromptPlaceholder: "Een prachtig landschap met bergen en zonsondergang...", stylePresets: "Stijlvoorinstellingen (optioneel):", urlPlaceholder: "https://voorbeeld.com/afbeelding.jpg", generatingWith: "Genereren met", mayTakeTime: "Dit kan 10-30 seconden duren", }, ar: { editText: "تعديل النص", mentionToAI: "إشارة للذكاء الاصطناعي", replaceImage: "استبدال الصورة", chooseFromLibrary: "اختر من المكتبة", changeUrl: "تغيير الرابط", generateWithAI: "إنشاء بالذكاء الاصطناعي", uploadImage: "رفع صورة", imageUrl: "رابط الصورة", cancel: "إلغاء", apply: "تطبيق", generate: "🎨 إنشاء", generating: "جاري الإنشاء...", enterImageUrl: "أدخل رابط الصورة...", chooseModel: "اختر النموذج:", describeImage: "صف صورتك:", aiPromptPlaceholder: "منظر طبيعي جميل مع الجبال وغروب الشمس...", stylePresets: "أنماط مسبقة (اختياري):", urlPlaceholder: "https://example.com/image.jpg", generatingWith: "إنشاء باستخدام", mayTakeTime: "قد يستغرق هذا 10-30 ثانية", }, zh: { editText: "编辑文本", mentionToAI: "提及AI", replaceImage: "替换图片", chooseFromLibrary: "从库中选择", changeUrl: "更改链接", generateWithAI: "AI生成", uploadImage: "上传图片", imageUrl: "图片链接", cancel: "取消", apply: "应用", generate: "🎨 生成", generating: "生成中...", enterImageUrl: "输入图片链接...", chooseModel: "选择模型:", describeImage: "描述你的图片:", aiPromptPlaceholder: "美丽的山景和日落...", stylePresets: "风格预设(可选):", urlPlaceholder: "https://example.com/image.jpg", generatingWith: "使用生成", mayTakeTime: "这可能需要10-30秒", }, ja: { editText: "テキストを編集", mentionToAI: "AIに言及", replaceImage: "画像を置換", chooseFromLibrary: "ライブラリから選択", changeUrl: "URLを変更", generateWithAI: "AIで生成", uploadImage: "画像をアップロード", imageUrl: "画像URL", cancel: "キャンセル", apply: "適用", generate: "🎨 生成", generating: "生成中...", enterImageUrl: "画像URLを入力...", chooseModel: "モデルを選択:", describeImage: "画像を説明してください:", aiPromptPlaceholder: "山と夕日の美しい風景...", stylePresets: "スタイルプリセット(オプション):", urlPlaceholder: "https://example.com/image.jpg", generatingWith: "生成中:", mayTakeTime: "10〜30秒かかる場合があります", }, el: { editText: "Επεξεργασία κειμένου", mentionToAI: "Αναφορά στην ΤΝ", replaceImage: "Αντικατάσταση εικόνας", chooseFromLibrary: "Επιλογή από βιβλιοθήκη", changeUrl: "Αλλαγή URL", generateWithAI: "Δημιουργία με ΤΝ", uploadImage: "Μεταφόρτωση εικόνας", imageUrl: "URL εικόνας", cancel: "Ακύρωση", apply: "Εφαρμογή", generate: "🎨 Δημιουργία", generating: "Δημιουργία...", enterImageUrl: "Εισάγετε URL εικόνας...", chooseModel: "Επιλογή μοντέλου:", describeImage: "Περιγράψτε την εικόνα σας:", aiPromptPlaceholder: "Ένα όμορφο τοπίο με βουνά και ηλιοβασίλεμα...", stylePresets: "Προεπιλογές στυλ (προαιρετικό):", urlPlaceholder: "https://example.com/image.jpg", generatingWith: "Δημιουργία με", mayTakeTime: "Μπορεί να χρειαστεί 10-30 δευτερόλεπτα", }, sq: { editText: "Redakto tekstin", mentionToAI: "Përmend AI-në", replaceImage: "Zëvendëso imazhin", chooseFromLibrary: "Zgjidh nga biblioteka", changeUrl: "Ndrysho URL-në", generateWithAI: "Gjenero me AI", uploadImage: "Ngarko imazh", imageUrl: "URL e imazhit", cancel: "Anulo", apply: "Apliko", generate: "🎨 Gjenero", generating: "Duke gjeneruar...", enterImageUrl: "Fut URL-në e imazhit...", chooseModel: "Zgjidh modelin:", describeImage: "Përshkruaj imazhin tënd:", aiPromptPlaceholder: "Një peizazh i bukur me male dhe perëndim dielli...", stylePresets: "Paracaktime stili (opsionale):", urlPlaceholder: "https://example.com/image.jpg", generatingWith: "Duke gjeneruar me", mayTakeTime: "Kjo mund të zgjasë 10-30 sekonda", }, id: { editText: "Edit teks", mentionToAI: "Sebut ke AI", replaceImage: "Ganti gambar", chooseFromLibrary: "Pilih dari perpustakaan", changeUrl: "Ubah URL", generateWithAI: "Hasilkan dengan AI", uploadImage: "Unggah gambar", imageUrl: "URL gambar", cancel: "Batal", apply: "Terapkan", generate: "🎨 Hasilkan", generating: "Menghasilkan...", enterImageUrl: "Masukkan URL gambar...", chooseModel: "Pilih model:", describeImage: "Deskripsikan gambar Anda:", aiPromptPlaceholder: "Pemandangan indah dengan gunung dan matahari terbenam...", stylePresets: "Preset gaya (opsional):", urlPlaceholder: "https://contoh.com/gambar.jpg", generatingWith: "Menghasilkan dengan", mayTakeTime: "Ini mungkin memakan waktu 10-30 detik", }, tr: { editText: "Metni düzenle", mentionToAI: "AI'ya bahset", replaceImage: "Resmi değiştir", chooseFromLibrary: "Kütüphaneden seç", changeUrl: "URL'yi değiştir", generateWithAI: "AI ile oluştur", uploadImage: "Resim yükle", imageUrl: "Resim URL'si", cancel: "İptal", apply: "Uygula", generate: "🎨 Oluştur", generating: "Oluşturuluyor...", enterImageUrl: "Resim URL'sini girin...", chooseModel: "Model seçin:", describeImage: "Resminizi tanımlayın:", aiPromptPlaceholder: "Dağlar ve gün batımı ile güzel bir manzara...", stylePresets: "Stil ön ayarları (isteğe bağlı):", urlPlaceholder: "https://ornek.com/resim.jpg", generatingWith: "İle oluşturuluyor", mayTakeTime: "Bu 10-30 saniye sürebilir", }, vi: { editText: "Chỉnh sửa văn bản", mentionToAI: "Đề cập đến AI", replaceImage: "Thay thế hình ảnh", chooseFromLibrary: "Chọn từ thư viện", changeUrl: "Thay đổi URL", generateWithAI: "Tạo với AI", uploadImage: "Tải lên hình ảnh", imageUrl: "URL hình ảnh", cancel: "Hủy", apply: "Áp dụng", generate: "🎨 Tạo", generating: "Đang tạo...", enterImageUrl: "Nhập URL hình ảnh...", chooseModel: "Chọn mô hình:", describeImage: "Mô tả hình ảnh của bạn:", aiPromptPlaceholder: "Phong cảnh đẹp với núi và hoàng hôn...", stylePresets: "Cài đặt phong cách (tùy chọn):", urlPlaceholder: "https://example.com/image.jpg", generatingWith: "Đang tạo với", mayTakeTime: "Có thể mất 10-30 giây", }, fa: { editText: "ویرایش متن", mentionToAI: "اشاره به هوش مصنوعی", replaceImage: "جایگزینی تصویر", chooseFromLibrary: "انتخاب از کتابخانه", changeUrl: "تغییر لینک", generateWithAI: "تولید با هوش مصنوعی", uploadImage: "آپلود تصویر", imageUrl: "لینک تصویر", cancel: "لغو", apply: "اعمال", generate: "🎨 تولید", generating: "در حال تولید...", enterImageUrl: "لینک تصویر را وارد کنید...", chooseModel: "انتخاب مدل:", describeImage: "تصویر خود را توصیف کنید:", aiPromptPlaceholder: "منظره زیبا با کوه‌ها و غروب آفتاب...", stylePresets: "پیش‌تنظیمات سبک (اختیاری):", urlPlaceholder: "https://example.com/image.jpg", generatingWith: "تولید با", mayTakeTime: "این ممکن است ۱۰-۳۰ ثانیه طول بکشد", }, }; // Supported languages list const supportedLangs = [ "en", "fr", "de", "es", "pt", "it", "nl", "ar", "zh", "ja", "el", "sq", "id", "tr", "vi", "fa", ]; // Detect language from document or parent frame function detectLanguage() { // Check document lang attribute const docLang = document.documentElement.lang || ""; const langCode = docLang.split("-")[0].toLowerCase(); if (supportedLangs.includes(langCode)) return langCode; // Check URL for locale pattern (e.g., /fr/, /en/) const pathMatch = window.location.pathname.match(/^\/([a-z]{2})\//); if (pathMatch && supportedLangs.includes(pathMatch[1])) return pathMatch[1]; // Default to English return "en"; } const currentLang = detectLanguage(); const t = (key) => translations[currentLang]?.[key] || translations.en[key] || key; /* ---------- helpers --------------------------------------------------- */ const css = (el, obj) => Object.assign(el.style, obj); function getElementPath(element) { const path = []; let current = element; let depth = 0; const maxDepth = 5; while (current && current !== document.body && depth < maxDepth) { let identifier = current.tagName.toLowerCase(); if (current.id) { identifier += `#${current.id}`; } else if (current.className && typeof current.className === "string") { const firstClass = current.className .trim() .split(" ") .filter((c) => c && !c.startsWith("_"))[0]; if (firstClass) { identifier += `.${firstClass}`; } } path.unshift(identifier); current = current.parentElement; depth++; } return path.join(" > "); } function isTextElement(el) { // Check if element contains ANY text (even nested) if (!el) return false; // Get all text content including nested elements const textContent = el.textContent || ""; // Check if it's a text-focused element const textTags = [ "H1", "H2", "H3", "H4", "H5", "H6", "P", "SPAN", "A", "BUTTON", "LI", "TD", "TH", "LABEL", "DIV", ]; const hasText = textContent.trim().length > 0; const isTextTag = textTags.includes(el.tagName); // If element has text and is a common text element, it's editable return hasText && (isTextTag || el.childNodes.length === 1); } function isImageElement(el) { return el && el.tagName === "IMG"; } // Shared menu styles (shadcn-inspired) function injectMenuStyles() { if (document.getElementById("kleap-menu-styles")) return; const style = document.createElement("style"); style.id = "kleap-menu-styles"; style.textContent = ` @keyframes kleapMenuIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } .kleap-menu { position: fixed; background: white; border: 1px solid hsl(240 5.9% 90%); border-radius: 8px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); z-index: 2147483648; padding: 4px; min-width: 180px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; animation: kleapMenuIn 0.15s ease-out; } .kleap-menu-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: 6px; cursor: pointer; transition: background-color 0.1s; color: hsl(240 10% 3.9%); font-size: 14px; line-height: 1; user-select: none; } .kleap-menu-item:hover { background: hsl(240 4.8% 95.9%); } .kleap-menu-item:active { background: hsl(240 5% 92%); } .kleap-menu-icon { width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: hsl(240 3.8% 46.1%); } .kleap-menu-icon svg { width: 16px; height: 16px; } .kleap-menu-separator { height: 1px; background: hsl(240 5.9% 90%); margin: 4px -4px; } .kleap-menu-label { padding: 8px 10px 4px; font-size: 12px; font-weight: 500; color: hsl(240 3.8% 46.1%); } `; document.head.appendChild(style); } // SVG icons (Lucide-style) const icons = { image: ``, link: ``, upload: ``, sparkles: ``, pencil: ``, messageCircle: ``, }; function createMenu(rect, items) { injectMenuStyles(); const menu = document.createElement("div"); menu.className = "kleap-menu"; // Position menu below element, or above if not enough space const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; const menuHeight = items.length * 36 + 8; // approximate let top, left; if (spaceBelow >= menuHeight || spaceBelow >= spaceAbove) { top = Math.min(rect.bottom + 6, window.innerHeight - menuHeight - 10); } else { top = Math.max(10, rect.top - menuHeight - 6); } left = Math.min(Math.max(10, rect.left), window.innerWidth - 200); css(menu, { top: `${top}px`, left: `${left}px`, }); items.forEach((item) => { if (item.separator) { const sep = document.createElement("div"); sep.className = "kleap-menu-separator"; menu.appendChild(sep); return; } const menuItem = document.createElement("div"); menuItem.className = "kleap-menu-item"; const icon = document.createElement("span"); icon.className = "kleap-menu-icon"; icon.innerHTML = item.icon; const label = document.createElement("span"); label.textContent = item.label; menuItem.appendChild(icon); menuItem.appendChild(label); menuItem.onclick = (e) => { e.stopPropagation(); closeMenu(); item.action(); }; menu.appendChild(menuItem); }); document.body.appendChild(menu); // Close menu on click outside or escape const closeMenu = () => { menu.remove(); document.removeEventListener("click", handleOutsideClick); document.removeEventListener("keydown", handleEscape); }; const handleOutsideClick = (e) => { if (!menu.contains(e.target)) { closeMenu(); } }; const handleEscape = (e) => { if (e.key === "Escape") { closeMenu(); } }; setTimeout(() => { document.addEventListener("click", handleOutsideClick); document.addEventListener("keydown", handleEscape); }, 0); return { menu, closeMenu }; } function enableImageEdit(img) { const rect = img.getBoundingClientRect(); createMenu(rect, [ { icon: icons.image, label: t("chooseFromLibrary"), action: () => showLibraryDialog(img), }, { icon: icons.upload, label: t("uploadImage"), action: () => showUploadDialog(img), }, { icon: icons.link, label: t("changeUrl"), action: () => showUrlDialog(img), }, { separator: true }, { icon: icons.sparkles, label: t("generateWithAI"), action: () => showAIDialog(img), }, ]); } function showTextMenu(el) { const rect = el.getBoundingClientRect(); createMenu(rect, [ { icon: icons.pencil, label: t("editText"), action: () => enableInlineEdit(el), }, { icon: icons.messageCircle, label: t("mentionToAI"), action: () => sendToAI(el), }, ]); } function sendToAI(el) { // Generate ID and name let id = el.dataset.kleapId; let name = el.dataset.kleapName; let path = el.dataset.kleapPath; if (!id) { const tag = el.tagName.toLowerCase(); const uniqueId = Math.random().toString(36).substr(2, 9); if (el.id) { id = `${tag}#${el.id}`; } else if (el.className && typeof el.className === "string") { const firstClass = el.className.trim().split(" ")[0]; id = `${tag}.${firstClass}-${uniqueId}`; } else { id = `${tag}-${uniqueId}`; } } if (!name) { const tag = el.tagName.toLowerCase(); if (el.className && typeof el.className === "string") { const classes = el.className .trim() .split(" ") .filter((c) => c && !c.startsWith("_")); const meaningfulClass = classes.find( (c) => !c.match( /^(flex|grid|block|inline|absolute|relative|fixed|sticky|w-|h-|p-|m-|text-|bg-|border-|rounded-|shadow-|opacity-|z-)/, ), ); name = meaningfulClass ? `${tag}.${meaningfulClass}` : `${tag}.${classes[0] || ""}`; } else { name = tag; } } if (!path) { path = getElementPath(el); } window.parent.postMessage( { type: "kleap-component-selected", id: id, name: name, path: path, tagName: el.tagName.toLowerCase(), className: el.className || "", textContent: el.textContent ? el.textContent.substring(0, 100) : "", hasChildren: el.children.length > 0, rect: { width: el.offsetWidth, height: el.offsetHeight, }, }, "*", ); } function showLibraryDialog(img) { // Send message to parent to open Asset Manager window.parent.postMessage( { type: "kleap-image-library-select", id: img.dataset.kleapId || `img-${Math.random().toString(36).substr(2, 9)}`, oldSrc: img.src, alt: img.alt || "", }, "*", ); } function showUrlDialog(img) { const dialog = createDialog(t("changeUrl")); const input = document.createElement("input"); input.type = "text"; input.value = img.src; input.placeholder = t("enterImageUrl"); css(input, { width: "100%", padding: "10px 12px", border: "1px solid #ddd", borderRadius: "8px", fontSize: "14px", marginBottom: "16px", boxSizing: "border-box", fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }); dialog.content.appendChild(input); dialog.onSave = () => { const newSrc = input.value.trim(); if (newSrc && newSrc !== img.src) { updateImage(img, newSrc); } }; input.focus(); input.select(); } function showUploadDialog(img) { // Open Asset Manager directly - it has full upload functionality window.parent.postMessage( { type: "kleap-image-library-select", id: img.dataset.kleapId || `img-${Math.random().toString(36).substr(2, 9)}`, oldSrc: img.src, alt: img.alt || "", }, "*", ); } function showAIDialog(img) { const dialog = createDialog(t("generateWithAI")); // Model selection const modelLabel = document.createElement("div"); css(modelLabel, { fontSize: "12px", color: "#666", marginBottom: "8px", fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }); modelLabel.textContent = t("chooseModel"); const modelContainer = document.createElement("div"); css(modelContainer, { display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: "8px", marginBottom: "16px", }); const models = [ { id: "flux-pro", name: "🚀 Flux Pro", desc: "Fast & High quality" }, { id: "flux-dev", name: "⚡ Flux Dev", desc: "Good balance" }, { id: "sdxl", name: "🎨 Stable Diffusion", desc: "Classic & reliable" }, { id: "playground-v2", name: "✨ Playground v2", desc: "Aesthetic focus", }, { id: "kandinsky", name: "🖼️ Kandinsky", desc: "Artistic style" }, { id: "dalle-3", name: "🤖 DALL-E 3", desc: "OpenAI (if available)" }, ]; let selectedModel = "flux-pro"; const modelButtons = []; models.forEach((model) => { const button = document.createElement("div"); css(button, { padding: "8px", borderRadius: "8px", border: "2px solid #ddd", cursor: "pointer", transition: "all 0.2s", textAlign: "center", background: model.id === selectedModel ? "#ff0055" : "white", color: model.id === selectedModel ? "white" : "#333", }); button.innerHTML = `
${model.name.split(" ")[0]}
${model.name.substring(model.name.indexOf(" ") + 1)}
${model.desc}
`; button.onclick = () => { selectedModel = model.id; // Update button styles modelButtons.forEach((btn) => { css(btn, { background: btn === button ? "#ff0055" : "white", color: btn === button ? "white" : "#333", border: btn === button ? "2px solid #ff0055" : "2px solid #ddd", }); }); }; modelButtons.push(button); modelContainer.appendChild(button); }); // Prompt textarea const promptLabel = document.createElement("div"); css(promptLabel, { fontSize: "12px", color: "#666", marginBottom: "8px", marginTop: "16px", fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }); promptLabel.textContent = t("describeImage"); const textarea = document.createElement("textarea"); textarea.placeholder = t("aiPromptPlaceholder"); css(textarea, { width: "100%", minHeight: "80px", padding: "10px 12px", border: "1px solid #ddd", borderRadius: "8px", fontSize: "14px", marginBottom: "12px", resize: "vertical", boxSizing: "border-box", fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }); // Style presets const styleLabel = document.createElement("div"); css(styleLabel, { fontSize: "12px", color: "#666", marginBottom: "8px", fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }); styleLabel.textContent = t("stylePresets"); const styleContainer = document.createElement("div"); css(styleContainer, { display: "flex", flexWrap: "wrap", gap: "6px", marginBottom: "16px", }); const styles = [ "Photorealistic", "Artistic", "Anime", "3D Render", "Watercolor", "Oil Painting", "Minimalist", "Vintage", ]; styles.forEach((style) => { const chip = document.createElement("div"); css(chip, { padding: "4px 10px", borderRadius: "12px", border: "1px solid #ddd", fontSize: "12px", cursor: "pointer", transition: "all 0.15s", background: "white", color: "#666", }); chip.textContent = style; chip.onclick = () => { const isSelected = chip.style.background === "rgb(255, 0, 85)"; css(chip, { background: isSelected ? "white" : "#ff0055", color: isSelected ? "#666" : "white", border: isSelected ? "1px solid #ddd" : "1px solid #ff0055", }); // Add/remove from prompt if (!isSelected) { if (!textarea.value.includes(style.toLowerCase())) { textarea.value = textarea.value.trim() + (textarea.value ? ", " : "") + style.toLowerCase() + " style"; } } else { textarea.value = textarea.value.replace( new RegExp(`,?\\s*${style.toLowerCase()}\\s*style`, "gi"), "", ); } }; styleContainer.appendChild(chip); }); // Append all elements dialog.content.appendChild(modelLabel); dialog.content.appendChild(modelContainer); dialog.content.appendChild(promptLabel); dialog.content.appendChild(textarea); dialog.content.appendChild(styleLabel); dialog.content.appendChild(styleContainer); dialog.saveBtn.textContent = t("generate"); dialog.onSave = () => { const prompt = textarea.value.trim(); if (prompt) { // Send to parent for AI generation with selected model window.parent.postMessage( { type: "kleap-image-ai-generate", id: img.dataset.kleapId || `img-${Math.random().toString(36).substr(2, 9)}`, prompt: prompt, model: selectedModel, oldSrc: img.src, }, "*", ); // Show generating state dialog.content.innerHTML = `
${t("generatingWith")} ${models.find((m) => m.id === selectedModel)?.name || "AI"}...
"${prompt.substring(0, 50)}${prompt.length > 50 ? "..." : ""}"
${t("mayTakeTime")}
`; dialog.saveBtn.style.display = "none"; } }; textarea.focus(); } function createDialog(title) { const overlay = document.createElement("div"); css(overlay, { position: "fixed", top: "0", left: "0", right: "0", bottom: "0", background: "rgba(0,0,0,0.5)", zIndex: "2147483647", display: "flex", alignItems: "center", justifyContent: "center", }); const dialog = document.createElement("div"); css(dialog, { background: "white", borderRadius: "16px", boxShadow: "0 20px 60px rgba(0,0,0,0.3)", minWidth: "400px", maxWidth: "500px", animation: "kleapDialogIn 0.2s ease-out", }); const header = document.createElement("div"); css(header, { padding: "20px 24px", borderBottom: "1px solid #eee", fontSize: "16px", fontWeight: "600", color: "#333", fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }); header.textContent = title; const content = document.createElement("div"); css(content, { padding: "20px 24px", }); const footer = document.createElement("div"); css(footer, { padding: "16px 24px", borderTop: "1px solid #eee", display: "flex", gap: "8px", justifyContent: "flex-end", }); const cancelBtn = document.createElement("button"); cancelBtn.textContent = t("cancel"); css(cancelBtn, { padding: "8px 16px", border: "1px solid #ddd", borderRadius: "8px", background: "white", color: "#666", fontSize: "14px", cursor: "pointer", fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }); const saveBtn = document.createElement("button"); saveBtn.textContent = t("apply"); css(saveBtn, { padding: "8px 20px", border: "none", borderRadius: "8px", background: "#ff0055", color: "white", fontSize: "14px", cursor: "pointer", fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', }); footer.appendChild(cancelBtn); footer.appendChild(saveBtn); dialog.appendChild(header); dialog.appendChild(content); dialog.appendChild(footer); overlay.appendChild(dialog); document.body.appendChild(overlay); const close = () => overlay.remove(); cancelBtn.onclick = close; overlay.onclick = (e) => { if (e.target === overlay) close(); }; const result = { content, saveBtn, close, onSave: null, }; saveBtn.onclick = () => { if (result.onSave) result.onSave(); close(); }; // Handle keyboard document.addEventListener("keydown", function handleKey(e) { if (e.key === "Escape") { close(); document.removeEventListener("keydown", handleKey); } }); return result; } function updateImage(img, newSrc) { const originalSrc = img.src; img.src = newSrc; // Send the change to parent window.parent.postMessage( { type: "kleap-image-edited", id: img.dataset.kleapId || `img-${Math.random().toString(36).substr(2, 9)}`, oldSrc: originalSrc, newSrc: newSrc, alt: img.alt || "", }, "*", ); } function enableInlineEdit(el) { // Store original state const originalText = el.textContent; const originalHTML = el.innerHTML; // Store ALL original styles const originalStyles = { outline: el.style.outline, outlineOffset: el.style.outlineOffset, boxShadow: el.style.boxShadow, cursor: el.style.cursor, contentEditable: el.contentEditable, // Don't modify background, border, or any other styles! }; // Make element editable el.contentEditable = "true"; el.focus(); // Add ONLY non-intrusive visual feedback css(el, { outline: "2px solid #ff0055", outlineOffset: "3px", cursor: "text", boxShadow: "0 0 0 4px rgba(255, 0, 85, 0.1), 0 4px 12px rgba(0, 0, 0, 0.08)", // DO NOT change background, border, borderRadius, or any other properties! }); // Select all text const range = document.createRange(); range.selectNodeContents(el); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); // Handle blur to save changes const handleBlur = () => { const newText = el.textContent; if (newText !== originalText) { // Show saving status on the border css(el, { outline: "2px solid #FFA500", // Orange for saving boxShadow: "0 0 0 4px rgba(255, 165, 0, 0.2), 0 4px 12px rgba(0, 0, 0, 0.08)", }); // Send the change to parent // Generate ID and name if not present const id = el.dataset.kleapId || `element-${Math.random().toString(36).substr(2, 9)}`; let name = el.dataset.kleapName || el.tagName.toLowerCase(); if ( !el.dataset.kleapName && el.className && typeof el.className === "string" ) { const classes = el.className .trim() .split(" ") .filter((c) => c); if (classes.length > 0) { name = `${el.tagName.toLowerCase()}.${classes[0]}`; } } window.parent.postMessage( { type: "kleap-text-edited", id: id, name: name, path: el.dataset.kleapPath || getElementPath(el), oldText: originalText, newText: newText, tagName: el.tagName.toLowerCase(), }, "*", ); // Show success after a moment setTimeout(() => { css(el, { outline: "2px solid #00C851", // Green for success boxShadow: "0 0 0 4px rgba(0, 200, 81, 0.2), 0 4px 12px rgba(0, 0, 0, 0.08)", }); // Remove all styles after showing success setTimeout(() => { el.contentEditable = originalStyles.contentEditable || "false"; css(el, { outline: originalStyles.outline || "", outlineOffset: originalStyles.outlineOffset || "", boxShadow: originalStyles.boxShadow || "", cursor: originalStyles.cursor || "", }); }, 1000); }, 500); } else { // Restore original HTML if no changes el.innerHTML = originalHTML; // Restore styles immediately if no changes el.contentEditable = originalStyles.contentEditable || "false"; css(el, { outline: originalStyles.outline || "", outlineOffset: originalStyles.outlineOffset || "", boxShadow: originalStyles.boxShadow || "", cursor: originalStyles.cursor || "", }); } el.removeEventListener("blur", handleBlur); el.removeEventListener("keydown", handleKeydown); }; // Handle escape key to cancel const handleKeydown = (e) => { if (e.key === "Escape") { e.preventDefault(); el.innerHTML = originalHTML; el.blur(); } }; el.addEventListener("blur", handleBlur); el.addEventListener("keydown", handleKeydown); } function makeOverlay() { overlay = document.createElement("div"); overlay.id = OVERLAY_ID; css(overlay, { position: "absolute", border: "2px solid #ff0055", background: "rgba(255,0,85,.05)", pointerEvents: "none", zIndex: "2147483647", // max borderRadius: "8px", boxShadow: "0 0 0 1px rgba(255,0,85,0.2), 0 4px 12px rgba(0,0,0,0.08)", transition: "all 0.2s cubic-bezier(0.4, 0, 0.2, 1)", }); label = document.createElement("div"); css(label, { position: "absolute", left: "50%", top: "100%", transform: "translateX(-50%) translateY(8px)", background: "linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%)", color: "#fff", fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", fontSize: "13px", fontWeight: "500", lineHeight: "1.4", padding: "0", borderRadius: "12px", boxShadow: "0 8px 32px rgba(0,0,0,0.24), 0 2px 8px rgba(0,0,0,0.16), inset 0 1px 0 rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.08)", overflow: "hidden", minWidth: "180px", animation: "kleapFadeIn 0.2s ease-out", pointerEvents: "auto", // Enable clicks on the label }); overlay.appendChild(label); document.body.appendChild(overlay); // Add animation keyframes const style = document.createElement("style"); style.textContent = ` @keyframes kleapFadeIn { from { opacity: 0; transform: translateX(-50%) translateY(4px); } to { opacity: 1; transform: translateX(-50%) translateY(8px); } } #${OVERLAY_ID} .kleap-option { transition: all 0.15s ease; } #${OVERLAY_ID} .kleap-option:hover { background: rgba(255,255,255,0.06); } #${OVERLAY_ID} .kleap-option:active { transform: scale(0.98); } `; document.head.appendChild(style); } function updateOverlay(el) { if (!overlay) makeOverlay(); const rect = el.getBoundingClientRect(); css(overlay, { top: `${rect.top + window.scrollY}px`, left: `${rect.left + window.scrollX}px`, width: `${rect.width}px`, height: `${rect.height}px`, display: "block", }); // Clear previous contents while (label.firstChild) { label.removeChild(label.firstChild); } // Always show minimal info when hovering const info = document.createElement("div"); css(info, { padding: "8px 12px", fontSize: "12px", opacity: "0.9", }); // Get a descriptive name for the element let name = el.dataset.kleapName || el.tagName.toLowerCase(); // Try to get a better description if (!el.dataset.kleapName) { const tag = el.tagName.toLowerCase(); // Add class names if available if (el.className && typeof el.className === "string") { const classes = el.className .trim() .split(" ") .filter((c) => c && !c.startsWith("_")); if (classes.length > 0) { // Filter out utility classes const meaningfulClass = classes.find( (c) => !c.match( /^(flex|grid|block|inline|absolute|relative|fixed|sticky|w-|h-|p-|m-|text-|bg-|border-|rounded-|shadow-|opacity-|z-)/, ), ); name = meaningfulClass ? `${tag}.${meaningfulClass}` : `${tag}.${classes[0]}`; } } else if (el.id) { name = `${tag}#${el.id}`; } else if (el.getAttribute("aria-label")) { name = `${tag}[${el.getAttribute("aria-label")}]`; } else if (el.getAttribute("role")) { name = `${tag}[role=${el.getAttribute("role")}]`; } } let actionHint = ""; if (isTextElement(el)) { actionHint = " • Click for options"; } else if (isImageElement(el)) { actionHint = " • Click for options"; } info.textContent = name + actionHint; label.appendChild(info); } /* ---------- event handlers -------------------------------------------- */ function onMouseMove(e) { if (state.type !== "inspecting") return; let el = e.target; // Don't require data-kleap-id - any element can be selected // Skip only truly non-selectable elements const nonSelectableTags = [ "SCRIPT", "STYLE", "META", "LINK", "HTML", "BODY", ]; while (el && nonSelectableTags.includes(el.tagName)) { el = el.parentElement; } if (state.element === el) return; state.element = el; if (el) { updateOverlay(el); } else { if (overlay) overlay.style.display = "none"; } } function onClick(e) { if (state.type !== "inspecting" || !state.element) return; e.preventDefault(); e.stopPropagation(); const el = state.element; // If it's a text element, show text menu (edit or mention to AI) if (isTextElement(el)) { showTextMenu(el); deactivate(); } else if (isImageElement(el)) { // If it's an image, show image edit dialog enableImageEdit(el); deactivate(); } else { // For non-text elements, send to parent for AI editing // Generate better ID and name let id = el.dataset.kleapId; let name = el.dataset.kleapName; let path = el.dataset.kleapPath; // Generate ID if not present if (!id) { // Try to create a meaningful ID from element properties const tag = el.tagName.toLowerCase(); const uniqueId = Math.random().toString(36).substr(2, 9); if (el.id) { id = `${tag}#${el.id}`; } else if (el.className && typeof el.className === "string") { const firstClass = el.className.trim().split(" ")[0]; id = `${tag}.${firstClass}-${uniqueId}`; } else { id = `${tag}-${uniqueId}`; } } // Generate name if not present if (!name) { const tag = el.tagName.toLowerCase(); if (el.className && typeof el.className === "string") { const classes = el.className .trim() .split(" ") .filter((c) => c && !c.startsWith("_")); const meaningfulClass = classes.find( (c) => !c.match( /^(flex|grid|block|inline|absolute|relative|fixed|sticky|w-|h-|p-|m-|text-|bg-|border-|rounded-|shadow-|opacity-|z-)/, ), ); name = meaningfulClass ? `${tag}.${meaningfulClass}` : `${tag}.${classes[0] || ""}`; } else { name = tag; } } // Generate path if not present if (!path) { path = getElementPath(el); } window.parent.postMessage( { type: "kleap-component-selected", id: id, name: name, path: path, tagName: el.tagName.toLowerCase(), className: el.className || "", textContent: el.textContent ? el.textContent.substring(0, 100) : "", hasChildren: el.children.length > 0, rect: { width: el.offsetWidth, height: el.offsetHeight, }, }, "*", ); deactivate(); } } /* ---------- activation / deactivation --------------------------------- */ function activate() { if (state.type === "inactive") { window.addEventListener("mousemove", onMouseMove, true); window.addEventListener("click", onClick, true); } state = { type: "inspecting", element: null }; if (overlay) { overlay.style.display = "none"; } } function deactivate() { if (state.type === "inactive") return; window.removeEventListener("mousemove", onMouseMove, true); window.removeEventListener("click", onClick, true); if (overlay) { overlay.remove(); overlay = null; label = null; } state = { type: "inactive" }; } /* ---------- message bridge -------------------------------------------- */ window.addEventListener("message", (e) => { if (e.source !== window.parent) return; if (e.data.type === "activate-kleap-component-selector") activate(); if (e.data.type === "deactivate-kleap-component-selector") deactivate(); }); function initializeComponentSelector() { if (!document.body) { console.error( "Kleap component selector initialization failed: document.body not found.", ); return; } setTimeout(() => { if (document.body.querySelector("[data-kleap-id]")) { window.parent.postMessage( { type: "kleap-component-selector-initialized", }, "*", ); console.debug("Kleap component selector initialized"); } else { console.warn( "Kleap component selector not initialized because no DOM elements were tagged", ); } }, 0); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initializeComponentSelector); } else { initializeComponentSelector(); } })();