Files
moreminimore-vibe/worker/dyad-component-selector-client.js
Will Chen c1aa6803ce Click to edit UI (#385)
- [x] add e2e test - happy case (make sure it clears selection and next
prompt is empty, and preview is cleared); de-selection case
- [x] shim - old & new file
- [x] upgrade path
- [x] add docs
- [x] add try-catch to parser script
- [x] make it work for next.js
- [x] extract npm package
- [x] make sure plugin doesn't apply in prod
2025-06-11 13:05:27 -07:00

211 lines
5.9 KiB
JavaScript

(() => {
const OVERLAY_ID = "__dyad_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 makeOverlay() {
overlay = document.createElement("div");
overlay.id = OVERLAY_ID;
css(overlay, {
position: "absolute",
border: "2px solid #7f22fe",
background: "rgba(0,170,255,.05)",
pointerEvents: "none",
zIndex: "2147483647", // max
borderRadius: "4px",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
});
label = document.createElement("div");
css(label, {
position: "absolute",
left: "0",
top: "100%",
transform: "translateY(4px)",
background: "#7f22fe",
color: "#fff",
fontFamily: "monospace",
fontSize: "12px",
lineHeight: "1.2",
padding: "3px 5px",
whiteSpace: "nowrap",
borderRadius: "4px",
boxShadow: "0 1px 4px rgba(0, 0, 0, 0.1)",
});
overlay.appendChild(label);
document.body.appendChild(overlay);
}
function updateOverlay(el, isSelected = false) {
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",
border: isSelected ? "3px solid #7f22fe" : "2px solid #7f22fe",
background: isSelected
? "rgba(127, 34, 254, 0.05)"
: "rgba(0,170,255,.05)",
});
css(label, {
background: "#7f22fe",
});
// Clear previous contents
while (label.firstChild) {
label.removeChild(label.firstChild);
}
if (isSelected) {
const editLine = document.createElement("div");
const svgNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("width", "12");
svg.setAttribute("height", "12");
svg.setAttribute("viewBox", "0 0 16 16");
svg.setAttribute("fill", "none");
Object.assign(svg.style, {
display: "inline-block",
verticalAlign: "-2px",
marginRight: "4px",
});
const path = document.createElementNS(svgNS, "path");
path.setAttribute(
"d",
"M8 0L9.48528 6.51472L16 8L9.48528 9.48528L8 16L6.51472 9.48528L0 8L6.51472 6.51472L8 0Z",
);
path.setAttribute("fill", "white");
svg.appendChild(path);
editLine.appendChild(svg);
editLine.appendChild(document.createTextNode("Edit with AI"));
label.appendChild(editLine);
}
const name = el.dataset.dyadName || "<unknown>";
const file = (el.dataset.dyadId || "").split(":")[0];
const nameEl = document.createElement("div");
nameEl.textContent = name;
label.appendChild(nameEl);
if (file) {
const fileEl = document.createElement("span");
css(fileEl, { fontSize: "10px", opacity: ".8" });
fileEl.textContent = file;
label.appendChild(fileEl);
}
}
/* ---------- event handlers -------------------------------------------- */
function onMouseMove(e) {
if (state.type !== "inspecting") return;
let el = e.target;
while (el && !el.dataset.dyadId) el = el.parentElement;
if (state.element === el) return;
state.element = el;
if (el) {
updateOverlay(el, false);
} else {
if (overlay) overlay.style.display = "none";
}
}
function onClick(e) {
if (state.type !== "inspecting" || !state.element) return;
e.preventDefault();
e.stopPropagation();
state = { type: "selected", element: state.element };
updateOverlay(state.element, true);
window.parent.postMessage(
{
type: "dyad-component-selected",
id: state.element.dataset.dyadId,
name: state.element.dataset.dyadName,
},
"*",
);
}
/* ---------- 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-dyad-component-selector") activate();
if (e.data.type === "deactivate-dyad-component-selector") deactivate();
});
function initializeComponentSelector() {
if (!document.body) {
console.error(
"Dyad component selector initialization failed: document.body not found.",
);
return;
}
setTimeout(() => {
if (document.body.querySelector("[data-dyad-id]")) {
window.parent.postMessage(
{
type: "dyad-component-selector-initialized",
},
"*",
);
console.debug("Dyad component selector initialized");
} else {
console.warn(
"Dyad component selector not initialized because no DOM elements were tagged",
);
}
}, 0);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initializeComponentSelector);
} else {
initializeComponentSelector();
}
})();