app-serene-lemur-run/public/_kleap/kleap-component-selector-cl...

1632 lines
50 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
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: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
link: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
upload: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>`,
sparkles: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg>`,
pencil: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>`,
messageCircle: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>`,
};
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 = `
<div style="font-size: 20px; margin-bottom: 4px;">${model.name.split(" ")[0]}</div>
<div style="font-size: 11px; font-weight: 500;">${model.name.substring(model.name.indexOf(" ") + 1)}</div>
<div style="font-size: 9px; opacity: 0.8; margin-top: 2px;">${model.desc}</div>
`;
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 = `
<div style="text-align: center; padding: 30px;">
<div style="font-size: 48px; margin-bottom: 12px; animation: spin 2s linear infinite;">✨</div>
<div style="color: #333; font-size: 16px; font-weight: 600;">${t("generatingWith")} ${models.find((m) => m.id === selectedModel)?.name || "AI"}...</div>
<div style="color: #666; font-size: 14px; margin-top: 8px;">"${prompt.substring(0, 50)}${prompt.length > 50 ? "..." : ""}"</div>
<div style="color: #999; font-size: 12px; margin-top: 12px;">${t("mayTakeTime")}</div>
</div>
<style>
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
`;
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();
}
})();