'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 = \`

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 };