'use strict';
/**
* Plan Canvas artifact SDK — the script injected into the reviewed artifact.
*
* The artifact runs in a sandboxed iframe without allow-same-origin, so this
* script can only talk to the chrome via postMessage. It renders all of its
* own UI inside a shadow root so it never annotates itself and never leaks
* styles into the artifact.
*/
function artifactSdkJs() {
return `'use strict';
(() => {
if (window.parent === window) return; // only meaningful inside the canvas
if (window.__eccPlanCanvasSdk) return;
window.__eccPlanCanvasSdk = true;
let annotate = true;
let card = null;
const post = msg => window.parent.postMessage(msg, '*');
// --- shadow-root UI host --------------------------------------------
const host = document.createElement('div');
host.setAttribute('data-ecc-plan-canvas', 'ui');
host.style.cssText = 'position:absolute;top:0;left:0;width:0;height:0;z-index:2147483647';
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = \`
Annotate selection
Cancel
Queue
Enter to queue · Cmd/Ctrl+Enter to queue & send
\`;
const attach = () => document.body ? document.body.appendChild(host) : null;
if (document.body) attach();
else document.addEventListener('DOMContentLoaded', attach);
const hl = root.querySelector('.hl');
const selhint = root.querySelector('.selhint');
const cardEl = root.querySelector('.card');
const cardTitle = cardEl.querySelector('h4');
const cardSnippet = cardEl.querySelector('.snippet');
const cardText = cardEl.querySelector('textarea');
// --- selectors & context ---------------------------------------------
const esc = v => (window.CSS && CSS.escape) ? CSS.escape(v) : v.replace(/[^a-zA-Z0-9_-]/g, '\\\\$&');
function selectorFor(el) {
const parts = [];
let node = el;
for (let depth = 0; node && node.nodeType === 1 && depth < 6; depth++) {
if (node.id) { parts.unshift('#' + esc(node.id)); return parts.join(' > '); }
const tag = node.tagName.toLowerCase();
if (tag === 'body' || tag === 'html') { parts.unshift(tag); break; }
let nth = 1;
let sib = node;
while ((sib = sib.previousElementSibling)) if (sib.tagName === node.tagName) nth++;
parts.unshift(tag + ':nth-of-type(' + nth + ')');
node = node.parentElement;
}
return parts.join(' > ');
}
function snippetFor(el) {
return (el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 200);
}
const INTERACTIVE = new Set(['button', 'input', 'select', 'textarea', 'option', 'label', 'summary', 'a']);
function isInteractive(el) {
let node = el;
while (node && node.nodeType === 1) {
if (INTERACTIVE.has(node.tagName.toLowerCase()) || node.isContentEditable) return true;
node = node.parentElement;
}
return false;
}
const isOurs = el => el === host || host.contains(el);
// --- annotation card ---------------------------------------------------
function openCard(target) {
card = target;
cardTitle.textContent = target.kindLabel;
cardSnippet.textContent = target.anchor.snippet || target.anchor.selector;
cardText.value = '';
cardEl.style.display = 'block';
const x = Math.min(target.x, window.innerWidth - 320) + window.scrollX;
const y = target.y + 12 + window.scrollY;
cardEl.style.left = Math.max(8, x) + 'px';
cardEl.style.top = y + 'px';
cardText.focus();
}
function closeCard() {
card = null;
cardEl.style.display = 'none';
}
function queueCard(sendNow) {
if (!card) return;
const text = cardText.value.trim();
if (!text) { cardText.focus(); return; }
post({
type: sendNow ? 'pc:queue-and-send' : 'pc:queue',
item: { kind: 'annotation', text, anchor: card.anchor }
});
closeCard();
}
cardEl.querySelector('.cancel').addEventListener('click', closeCard);
cardEl.querySelector('.queue').addEventListener('click', () => queueCard(false));
cardText.addEventListener('keydown', e => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); queueCard(true); }
else if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); queueCard(false); }
else if (e.key === 'Escape') closeCard();
});
// --- element hover / click ---------------------------------------------
document.addEventListener('mousemove', e => {
if (!annotate || card) { hl.style.display = 'none'; return; }
const el = e.target;
if (!el || isOurs(el) || el === document.body || el === document.documentElement || isInteractive(el)) {
hl.style.display = 'none';
return;
}
const rect = el.getBoundingClientRect();
hl.style.display = 'block';
hl.style.left = rect.left - 2 + 'px';
hl.style.top = rect.top - 2 + 'px';
hl.style.width = rect.width + 'px';
hl.style.height = rect.height + 'px';
}, true);
document.addEventListener('click', e => {
if (!annotate) return;
const el = e.target;
if (isOurs(el)) return;
if (card) { if (!cardEl.contains(e.composedPath()[0])) closeCard(); return; }
if (isInteractive(el)) return; // let controls behave natively
const selection = window.getSelection();
if (selection && !selection.isCollapsed) return; // handled by selection flow
if (el === document.body || el === document.documentElement) return;
e.preventDefault();
e.stopPropagation();
hl.style.display = 'none';
openCard({
kindLabel: 'Annotate <' + el.tagName.toLowerCase() + '>',
anchor: { selector: selectorFor(el), tag: el.tagName.toLowerCase(), snippet: snippetFor(el) },
x: e.clientX,
y: e.clientY
});
}, true);
// --- text selection -------------------------------------------------------
document.addEventListener('mouseup', e => {
if (!annotate || card || isOurs(e.target)) return;
setTimeout(() => {
const selection = window.getSelection();
const text = selection ? String(selection).replace(/\\s+/g, ' ').trim() : '';
if (!text || !selection.rangeCount) { selhint.style.display = 'none'; return; }
const rect = selection.getRangeAt(0).getBoundingClientRect();
selhint.style.display = 'block';
selhint.style.left = rect.left + window.scrollX + 'px';
selhint.style.top = rect.bottom + 6 + window.scrollY + 'px';
selhint.onclick = () => {
selhint.style.display = 'none';
const anchorNode = selection.anchorNode;
const el = anchorNode && anchorNode.nodeType === 1 ? anchorNode : anchorNode && anchorNode.parentElement;
openCard({
kindLabel: 'Annotate selection',
anchor: {
selector: el ? selectorFor(el) : 'body',
tag: 'text',
snippet: text.slice(0, 200),
textRange: { text: text.slice(0, 1000) }
},
x: rect.left,
y: rect.bottom
});
};
}, 0);
}, true);
document.addEventListener('selectionchange', () => {
const selection = window.getSelection();
if (!selection || selection.isCollapsed) selhint.style.display = 'none';
});
// --- chrome bridge ---------------------------------------------------------
window.addEventListener('message', e => {
const msg = e.data || {};
if (msg.type === 'pc:set-mode') {
annotate = Boolean(msg.annotate);
if (!annotate) { hl.style.display = 'none'; selhint.style.display = 'none'; closeCard(); }
} else if (msg.type === 'pc:restore-scroll') {
window.scrollTo(msg.x || 0, msg.y || 0);
}
});
document.addEventListener('keydown', e => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'i') {
e.preventDefault();
post({ type: 'pc:toggle-mode' });
} else if (e.key === 'Escape' && card) closeCard();
}, true);
let scrollTimer = null;
window.addEventListener('scroll', () => {
if (scrollTimer) return;
scrollTimer = setTimeout(() => {
scrollTimer = null;
post({ type: 'pc:scroll', x: window.scrollX, y: window.scrollY });
}, 150);
}, { passive: true });
post({ type: 'pc:ready' });
})();`;
}
module.exports = { artifactSdkJs };