UNPKG

untitled-bridge

Version:

Plugin-based builder bridge with live design token editing, smart element selection, source tracking, and two-way live editing support

587 lines (523 loc) 25.3 kB
/* * Untitled Bridge v3.0 - Plugin Architecture + Selection */ (function () { try { // If running in a browser, log once after DOM ready; else no-op if (typeof window === "undefined") return; var ready = function () { try { console.log("[untitled-bridge] v3.0.0 - Plugin Architecture Enabled"); // Handshake + selection var url; try { url = new URL(window.location.href); } catch {} var token = url ? (url.searchParams.get("builderSession") || "") : ""; function post(type, payload) { try { if (window.parent && window.parent !== window) { window.parent.postMessage( Object.assign({ protocol: "untitled-bridge", version: "0.3.0", type: type, token: token }, payload || {}), "*" ); } } catch {} } // === Plugin Architecture === var Bridge = (function () { var plugins = {}; var state = { token: token, handshook: false, allowedOrigin: "*" }; function registerPlugin(name, plugin) { try { if (plugins[name]) return; // idempotent plugins[name] = plugin; if (plugin.init) plugin.init({ post: post, state: state, getCssVar: getCssVar, setCssVar: setCssVar }); console.log("[untitled-bridge] Plugin registered:", name); } catch (e) { console.warn("[untitled-bridge] Plugin init failed:", name, e); } } function dispatchMessage(ev) { try { // Origin check (tighten in production via state.allowedOrigin) if (state.allowedOrigin !== "*" && ev.origin !== state.allowedOrigin) return; var msg = ev.data || {}; if (msg.protocol !== "untitled-bridge") return; if (msg.token && state.token && msg.token !== state.token) return; // Route by type namespace: "designTokens/applyPatch", "ui/setThemeMode", etc. var type = msg.type || ""; var ns = type.split("/")[0]; var plugin = plugins[ns]; if (plugin && plugin.onMessage) { plugin.onMessage(msg, { post: post, state: state, ev: ev, getCssVar: getCssVar, setCssVar: setCssVar }); } } catch (e) { console.warn("[untitled-bridge] Message dispatch failed:", e); } } function getCssVar(name) { try { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); } catch { return ""; } } function setCssVar(name, value, scopeSelector) { try { if (!scopeSelector || scopeSelector === ":root") { document.documentElement.style.setProperty(name, value); } else { // Write into a scoped style block (e.g. :root[data-theme="dark"]) var id = "ub-scope-" + btoa(scopeSelector).replace(/=/g, ""); var style = document.getElementById(id) || (function(){ var s = document.createElement("style"); s.id = id; document.head.appendChild(s); return s; })(); var css = style.textContent || ""; var blockRe = new RegExp(scopeSelector.replace(/[[\]{}()*+?.\\^$|]/g, "\\$&") + "\\s*\\{([\\s\\S]*?)\\}", "m"); var decl = name + ":" + value + ";"; if (blockRe.test(css)) { style.textContent = css.replace(blockRe, function(_, body){ return scopeSelector + "{" + upsertDecl(body, name, value) + "}"; }); } else { style.textContent = css + "\n" + scopeSelector + "{" + decl + "}"; } } } catch (e) { console.warn("[untitled-bridge] setCssVar failed:", e); } } function upsertDecl(body, name, value) { var lines = body.split(";").map(function(s){return s.trim();}).filter(Boolean); var idx = lines.findIndex(function(l){return l.indexOf(name + ":") === 0;}); var decl = name + ":" + value; if (idx >= 0) lines[idx] = decl; else lines.push(decl); return lines.join(";") + ";"; } return { registerPlugin: registerPlugin, dispatchMessage: dispatchMessage, state: state, getCssVar: getCssVar, setCssVar: setCssVar }; })(); // Expose Bridge globally try { window.UntitledBridge = window.UntitledBridge || {}; Object.assign(window.UntitledBridge, Bridge); } catch {} // Hook plugin message dispatcher window.addEventListener("message", function (ev) { try { Bridge.dispatchMessage(ev); } catch {} }); // Send hello immediately post("hello", { url: window.location.href }); // Elements to exclude from selection (structural/layout elements) var EXCLUDE_TAGS = ['html', 'body', 'head', 'script', 'style', 'meta', 'link', 'title', 'main', 'section', 'div', 'header', 'footer', 'nav', 'aside', 'article']; // Elements that are specifically for image selection var IMAGE_TAGS = ['img', 'picture', 'figure']; function isSelectable(el) { try { if (!el || !el.tagName) return false; var tag = el.tagName.toLowerCase(); // Exclude certain tags if (EXCLUDE_TAGS.includes(tag)) return false; // Exclude elements with no visible content or very small size var rect = el.getBoundingClientRect(); if (rect.width < 1 || rect.height < 1) return false; // Exclude elements that are not visible (safe check) var style = window.getComputedStyle(el); if (style && (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0')) return false; return true; } catch { return false; } } function isImageElement(el) { try { if (!el || !el.tagName) return false; var tag = el.tagName.toLowerCase(); // Direct image tags if (IMAGE_TAGS.includes(tag)) return true; // Check if element contains an image or has background-image if (el.querySelector && el.querySelector('img')) return true; var style = window.getComputedStyle(el); if (style && style.backgroundImage && style.backgroundImage !== 'none') return true; return false; } catch { return false; } } function getImageInfo(el) { try { var tag = el.tagName.toLowerCase(); var info = { isImage: false, currentSrc: null, alt: null, hasBackgroundImage: false, backgroundImage: null }; if (IMAGE_TAGS.includes(tag)) { info.isImage = true; info.currentSrc = el.src || el.currentSrc || null; info.alt = el.alt || null; } else { // Check for nested img var img = el.querySelector && el.querySelector('img'); if (img) { info.isImage = true; info.currentSrc = img.src || img.currentSrc || null; info.alt = img.alt || null; } // Check for background image var style = window.getComputedStyle(el); if (style && style.backgroundImage && style.backgroundImage !== 'none') { info.hasBackgroundImage = true; info.backgroundImage = style.backgroundImage; } } return info; } catch { return { isImage: false, currentSrc: null, alt: null, hasBackgroundImage: false, backgroundImage: null }; } } function send(type, el) { try { if (!isSelectable(el)) return; var r = el.getBoundingClientRect(); var imageInfo = getImageInfo(el); post(type, { rect: { left: r.left, top: r.top, width: r.width, height: r.height }, meta: { tag: (el.tagName || "").toLowerCase(), id: el.id || "", name: el.getAttribute && el.getAttribute("name") || "", className: el.className || "", textContent: el.textContent ? el.textContent.trim().substring(0, 50) : "", isImage: imageInfo.isImage, imageInfo: imageInfo }, page: { path: window.location.pathname } }); } catch {} } var last = null; window.addEventListener("pointermove", function (e) { try { var el = document.elementFromPoint(e.clientX, e.clientY); if (el && isSelectable(el) && el !== last) { last = el; send("hover", el); } } catch {} }, { passive: true }); window.addEventListener("click", function (e) { try { var el = document.elementFromPoint(e.clientX, e.clientY); if (el && isSelectable(el)) { send("select", el); } } catch {} }, true); window.addEventListener("scroll", function () { try { if (last) send("hover", last); } catch {} }, { passive: true }); // === DesignSync Plugin === // Handles live design token updates via postMessage var designSyncPlugin = { init: function(ctx) { console.log("[untitled-bridge] DesignSync plugin initialized"); }, onMessage: function(msg, ctx) { var type = msg.type || ""; // Apply live token patch: { "--background": { base: "oklch(...)", modes: { dark: "..." } } } if (type === "designTokens/applyPatch") { try { var patch = msg.patch || {}; console.log("[untitled-bridge] Applying design token patch:", patch); Object.keys(patch).forEach(function(cssVarName) { // cssVarName is already a CSS variable name like "--background", "--primary", etc. var spec = patch[cssVarName] || {}; // Apply base value to :root if (spec.base !== undefined) { var cssValue = toCssValue(spec.base); console.log("[untitled-bridge] Setting", cssVarName, "=", cssValue, "on :root"); ctx.setCssVar(cssVarName, cssValue, ":root"); } // Apply dark mode value if present if (spec.modes && spec.modes.dark !== undefined) { var darkValue = toCssValue(spec.modes.dark); console.log("[untitled-bridge] Setting", cssVarName, "=", darkValue, "on .dark"); ctx.setCssVar(cssVarName, darkValue, ".dark"); } }); // Send acknowledgement ctx.post({ protocol: "untitled-bridge", version: "0.3.0", type: "designTokens/ack", token: ctx.state.token, requestId: msg.requestId }); } catch (e) { console.warn("[untitled-bridge] applyPatch failed:", e); } return; } // Set theme mode (light/dark toggle) if (type === "ui/setThemeMode") { try { var html = document.documentElement; if (msg.mode === "dark") { html.setAttribute("data-theme", "dark"); } else { html.removeAttribute("data-theme"); } console.log("[untitled-bridge] Theme mode set to:", msg.mode || "light"); } catch (e) { console.warn("[untitled-bridge] setThemeMode failed:", e); } return; } // Request current CSS variable snapshot (for debugging/inspect) if (type === "designTokens/requestSnapshot") { try { var snapshot = {}; var tokenMap = { "color.background": "--background", "color.foreground": "--foreground", "color.primary": "--primary", "radius": "--radius" }; Object.keys(tokenMap).forEach(function(tokenKey) { var varName = tokenMap[tokenKey]; var value = ctx.getCssVar(varName); if (value) snapshot[tokenKey] = value; }); ctx.post({ protocol: "untitled-bridge", version: "0.3.0", type: "designTokens/snapshot", token: ctx.state.token, requestId: msg.requestId, snapshot: snapshot }); } catch (e) { console.warn("[untitled-bridge] requestSnapshot failed:", e); } return; } } }; // Helper: Convert token value to CSS string function toCssValue(v) { if (v == null) return ""; if (typeof v === "string") return v; if (typeof v === "number") return v + "px"; if (v.oklch) return v.oklch; if (typeof v.r === "number") { return "rgba(" + v.r + "," + v.g + "," + v.b + "," + (v.a == null ? 1 : v.a) + ")"; } if (typeof v.h === "number") { return "hsl(" + v.h + " " + v.s + "% " + v.l + "% / " + (v.a == null ? 1 : v.a) + ")"; } return String(v); } // === ImageUpdate Plugin === // Handles live image updates via postMessage var imageUpdatePlugin = { init: function(ctx) { console.log("[untitled-bridge] ImageUpdate plugin initialized"); }, onMessage: function(msg, ctx) { var type = msg.type || ""; // Update image source: { elementId: "img-123", newSrc: "https://..." } if (type === "image/updateSrc") { try { console.log("[untitled-bridge] 🖼️ Received image/updateSrc message:", msg); var elementId = msg.elementId; var newSrc = msg.newSrc; var element = document.getElementById(elementId); console.log("[untitled-bridge] 🔍 Looking for element with ID:", elementId); console.log("[untitled-bridge] 📍 Element found:", element); console.log("[untitled-bridge] 🆔 Element tag:", element ? element.tagName : "N/A"); // If element not found by ID, try to find by other means if (!element) { console.log("[untitled-bridge] 🔍 Element not found by ID, trying alternative methods..."); // Try to find by data attributes or other selectors var alternatives = [ 'img[data-element-id="' + elementId + '"]', 'img[data-id="' + elementId + '"]', 'img[alt*="' + elementId + '"]', 'img[src*="' + elementId + '"]' ]; for (var i = 0; i < alternatives.length; i++) { element = document.querySelector(alternatives[i]); if (element) { console.log("[untitled-bridge] ✅ Found element using alternative selector:", alternatives[i]); break; } } // If still not found, try to find any img element (fallback) if (!element) { console.log("[untitled-bridge] 🔍 Trying fallback - finding any img element"); var allImages = document.querySelectorAll('img'); if (allImages.length > 0) { element = allImages[0]; // Use first image as fallback console.log("[untitled-bridge] ⚠️ Using first available img as fallback:", element); } } } if (element) { // Update img src if (element.tagName.toLowerCase() === 'img') { console.log("[untitled-bridge] 🖼️ Updating img src directly"); // Handle Next.js Image optimization if (element.srcset || element.dataset.nimg) { console.log("[untitled-bridge] 🔄 Detected Next.js Image, completely replacing behavior"); // Store original attributes for potential restoration var originalSrc = element.src; var originalSrcset = element.srcset; var originalDataNimg = element.dataset.nimg; // Completely clear Next.js optimization attributes element.removeAttribute('srcset'); element.removeAttribute('data-nimg'); element.removeAttribute('data-nimg'); element.removeAttribute('decoding'); element.removeAttribute('loading'); // Clear any style attributes that might interfere element.style.color = ''; element.style.color = ''; // Set the new src element.src = newSrc; // Force a complete re-render element.style.display = 'none'; element.offsetHeight; // Force reflow element.style.display = ''; // Add a data attribute to mark this as manually updated element.setAttribute('data-manual-update', 'true'); console.log("[untitled-bridge] ✅ Completely replaced Next.js Image behavior for", elementId); console.log("[untitled-bridge] 🔄 New src:", newSrc); } else { element.src = newSrc; console.log("[untitled-bridge] ✅ Updated regular img src for", elementId, "to", newSrc); } console.log("[untitled-bridge] ✅ Updated img src for", elementId, "to", newSrc); } // Update nested img else { console.log("[untitled-bridge] 🔍 Looking for nested img in element"); var img = element.querySelector('img'); if (img) { console.log("[untitled-bridge] 🖼️ Found nested img, updating src"); // Handle Next.js Image optimization for nested img if (img.srcset || img.dataset.nimg) { console.log("[untitled-bridge] 🔄 Detected Next.js Image in nested element, updating all attributes"); // Clear srcset to prevent Next.js from overriding img.srcset = ''; img.removeAttribute('srcset'); // Clear data attributes that might interfere if (img.dataset.nimg) { img.removeAttribute('data-nimg'); } // Force the src update img.src = newSrc; // Trigger a reflow to ensure the change is visible img.style.display = 'none'; img.offsetHeight; // Force reflow img.style.display = ''; } else { img.src = newSrc; } console.log("[untitled-bridge] ✅ Updated nested img src for", elementId, "to", newSrc); } else { console.warn("[untitled-bridge] ⚠️ No nested img found in element:", elementId); } } // Send acknowledgement ctx.post({ protocol: "untitled-bridge", version: "0.3.0", type: "image/updateAck", token: ctx.state.token, elementId: elementId, newSrc: newSrc }); console.log("[untitled-bridge] 📤 Sent acknowledgement"); } else { console.warn("[untitled-bridge] ❌ Element not found for image update:", elementId); console.log("[untitled-bridge] 🔍 Available elements with IDs:", Array.from(document.querySelectorAll('[id]')).map(el => el.id) ); console.log("[untitled-bridge] 🔍 All img elements:", Array.from(document.querySelectorAll('img')).map(el => ({ id: el.id, src: el.src, alt: el.alt })) ); } } catch (e) { console.warn("[untitled-bridge] ❌ image/updateSrc failed:", e); } return; } // Update background image: { elementId: "div-123", newBgImage: "url('https://...')" } if (type === "image/updateBackground") { try { console.log("[untitled-bridge] 🖼️ Received image/updateBackground message:", msg); var elementId = msg.elementId; var newBgImage = msg.newBgImage; var element = document.getElementById(elementId); console.log("[untitled-bridge] 🔍 Looking for element with ID:", elementId); console.log("[untitled-bridge] 📍 Element found:", element); console.log("[untitled-bridge] 🆔 Element tag:", element ? element.tagName : "N/A"); if (element) { console.log("[untitled-bridge] 🖼️ Updating background image"); element.style.backgroundImage = newBgImage; console.log("[untitled-bridge] ✅ Updated background image for", elementId, "to", newBgImage); // Send acknowledgement ctx.post({ protocol: "untitled-bridge", version: "0.3.0", type: "image/updateBgAck", token: ctx.state.token, elementId: elementId, newBgImage: newBgImage }); console.log("[untitled-bridge] 📤 Sent background image acknowledgement"); } else { console.warn("[untitled-bridge] ❌ Element not found for background image update:", elementId); console.log("[untitled-bridge] 🔍 Available elements with IDs:", Array.from(document.querySelectorAll('[id]')).map(el => el.id) ); } } catch (e) { console.warn("[untitled-bridge] ❌ image/updateBackground failed:", e); } return; } } }; // Register the plugins Bridge.registerPlugin("designTokens", designSyncPlugin); Bridge.registerPlugin("ui", designSyncPlugin); // Also handles ui/* messages Bridge.registerPlugin("image", imageUpdatePlugin); } catch (err) { console.warn("[untitled-bridge] bridge failed:", err); } }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", ready); } else { ready(); } } catch (err) { console.warn("[untitled-bridge] init error:", err); } })(); module.exports = {};