(() => { 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" }; /* ---------- 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'; } function enableImageEdit(img) { // Get image position const rect = img.getBoundingClientRect(); // Create context menu const menu = document.createElement('div'); css(menu, { position: 'fixed', top: `${Math.min(rect.bottom + 8, window.innerHeight - 200)}px`, left: `${Math.min(rect.left, window.innerWidth - 250)}px`, background: 'white', borderRadius: '12px', boxShadow: '0 8px 32px rgba(0,0,0,0.15)', zIndex: '2147483648', padding: '8px', minWidth: '240px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', animation: 'kleapMenuSlideIn 0.2s ease-out', }); // Add animation const style = document.createElement('style'); style.textContent = ` @keyframes kleapMenuSlideIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } .kleap-menu-item { padding: 10px 12px; border-radius: 8px; cursor: pointer; transition: background 0.15s; display: flex; align-items: center; gap: 12px; color: #333; font-size: 14px; } .kleap-menu-item:hover { background: #f5f5f5; } .kleap-menu-icon { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; } `; document.head.appendChild(style); // Menu items with icons const menuItems = [ { icon: '📚', label: 'Choose from Library', action: () => showLibraryDialog(img) }, { icon: '🔗', label: 'Change URL', action: () => showUrlDialog(img) }, { icon: '📤', label: 'Upload Image', action: () => showUploadDialog(img) }, { icon: '✨', label: 'Generate with AI', action: () => showAIDialog(img) } ]; menuItems.forEach(item => { const menuItem = document.createElement('div'); menuItem.className = 'kleap-menu-item'; const icon = document.createElement('span'); icon.className = 'kleap-menu-icon'; icon.textContent = item.icon; const label = document.createElement('span'); label.textContent = item.label; menuItem.appendChild(icon); menuItem.appendChild(label); menuItem.onclick = () => { menu.remove(); style.remove(); item.action(); }; menu.appendChild(menuItem); }); document.body.appendChild(menu); // Close menu on click outside const closeMenu = (e) => { if (!menu.contains(e.target) && e.target !== img) { menu.remove(); style.remove(); document.removeEventListener('click', closeMenu); } }; setTimeout(() => { document.addEventListener('click', closeMenu); }, 100); } 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('Change Image URL'); const input = document.createElement('input'); input.type = 'text'; input.value = img.src; input.placeholder = 'Enter image URL...'; 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('Generate with AI'); // 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 = 'Choose Model:'; 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 = 'Describe your image:'; const textarea = document.createElement('textarea'); textarea.placeholder = 'A beautiful landscape with mountains and a sunset...'; 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 = 'Style presets (optional):'; 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 = '🎨 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 = `
Generating with ${models.find(m => m.id === selectedModel)?.name || 'AI'}...
"${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}"
This may take 10-30 seconds
`; 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 = '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 = '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 to edit text"; } else if (isImageElement(el)) { actionHint = " • Click to change image"; } 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, enable inline edit directly if (isTextElement(el)) { enableInlineEdit(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(); } })();