From cb7c69265a16b1dadca972577dfb53c0d306b85f Mon Sep 17 00:00:00 2001 From: kleap-admin Date: Fri, 16 Jan 2026 16:05:41 +0000 Subject: [PATCH] Update public/_kleap/kleap-component-selector-client.js --- .../_kleap/kleap-component-selector-client.js | 1631 +++++++++++++++++ 1 file changed, 1631 insertions(+) create mode 100644 public/_kleap/kleap-component-selector-client.js diff --git a/public/_kleap/kleap-component-selector-client.js b/public/_kleap/kleap-component-selector-client.js new file mode 100644 index 0000000..40d23cc --- /dev/null +++ b/public/_kleap/kleap-component-selector-client.js @@ -0,0 +1,1631 @@ +(() => { + 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(); + } +})();