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
JavaScript
/*
* 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 = {};