made tiny updates

This commit is contained in:
SinachPat
2026-05-01 09:54:14 +01:00
parent 84ddd2e248
commit 548cd11719
9 changed files with 2072 additions and 19 deletions
+123
View File
@@ -0,0 +1,123 @@
// ── Universal DOM Inspector Script ───────────────────────────────────────────
// Generates a self-contained IIFE that can be injected into ANY HTML page
// (React, Vue, static HTML — anything) via the CLI proxy or the live-sdk.
//
// What it does:
// • Intercepts clicks → sends COMPONENT_SELECTED + ELEMENT_STYLES
// • Responds to PATCH_ELEMENT_STYLE / REMOVE_ELEMENT / REQUEST_ELEMENT_STYLES
// • Discovers routes from <a> links → sends ROUTES_DISCOVERED
// • Applies SET_DESIGN_TOKENS as CSS custom properties on :root
// • Sends READY once the DOM is interactive
//
// No React dependency. Works for any HTML page loaded in an Originmain iframe.
import { RENDERER_SOURCE, HOST_SOURCE } from './protocol.js';
/** Returns the injectable DOM inspector script as a string (IIFE). */
export function buildDomInspectorScript(): string {
return `(function(){
/* ── Guard ─────────────────────────────────── */
var PREFIX='om:',SRC=${JSON.stringify(RENDERER_SOURCE)},HOST=${JSON.stringify(HOST_SOURCE)};
if(window.parent===window)return;
var aid='';
try{var n=window.name;if(typeof n==='string'&&n.indexOf(PREFIX)===0)aid=n.slice(PREFIX.length);}catch(_){}
if(!aid)return;
/* ── postMessage ────────────────────────────── */
function post(m){try{window.parent.postMessage({source:SRC,artboardId:aid,message:m},'*');}catch(_){}}
/* ── Element IDs ────────────────────────────── */
var seq=0;
function assignId(el){var cur=el.getAttribute('data-om');if(cur)return cur;var id=(++seq).toString(36);try{el.setAttribute('data-om',id);}catch(_){}return id;}
function byId(id){return document.querySelector('[data-om="'+id+'"]');}
/* ── Component name (React-aware) ───────────── */
function nameOf(el){
var ks=Object.keys(el);
for(var i=0;i<ks.length;i++){
if(ks[i].indexOf('__reactFiber')!==0)continue;
var f=el[ks[i]];
while(f){var t=f.type;if(typeof t==='function'){var nm=t.displayName||t.name;if(nm&&!/^[a-z]/.test(nm))return nm;}f=f.return;}
}
var tag=(el.tagName||'el').toLowerCase();
var id2=el.id?'#'+el.id:'';
var cls='';
if(el.className&&typeof el.className==='string'){var fc=el.className.trim().split(/\\s+/)[0];if(fc)cls='.'+fc;}
return tag+(id2||cls);
}
/* ── CSS capture ────────────────────────────── */
var PROPS=['width','height','display','position','top','left','right','bottom',
'padding-top','padding-right','padding-bottom','padding-left',
'margin-top','margin-right','margin-bottom','margin-left',
'background-color','color','font-size','font-weight','font-family',
'line-height','letter-spacing','text-align','border-radius',
'border-width','border-color','border-style','box-shadow','opacity',
'flex-direction','align-items','justify-content','gap','overflow','z-index'];
function capStyles(el){var cs=window.getComputedStyle(el),out={};for(var i=0;i<PROPS.length;i++){var v=cs.getPropertyValue(PROPS[i]);if(v)out[PROPS[i]]=v;}return out;}
function capRect(el){var r=el.getBoundingClientRect();return{x:r.left,y:r.top,width:r.width,height:r.height};}
/* ── Selection highlight ────────────────────── */
var selEl=null;
function ensureCss(){
if(document.getElementById('om-css'))return;
var s=document.createElement('style');s.id='om-css';
s.textContent='[data-om-s]{outline:2px solid rgba(51,133,255,0.85)!important;outline-offset:1px!important;}';
(document.head||document.documentElement).appendChild(s);
}
function selectEl(el){if(selEl)selEl.removeAttribute('data-om-s');selEl=el;if(el)el.setAttribute('data-om-s','');}
/* ── Click handler ──────────────────────────── */
document.addEventListener('click',function(e){
e.preventDefault();e.stopPropagation();
var el=e.target;
if(!el||el===document.documentElement||el===document.body){selectEl(null);post({type:'COMPONENT_DESELECTED'});return;}
var id=assignId(el);
selectEl(el);
post({type:'COMPONENT_SELECTED',nodeId:id,nodeName:nameOf(el),rect:capRect(el)});
post({type:'ELEMENT_STYLES',nodeId:id,styles:capStyles(el)});
},true);
/* ── Host messages ──────────────────────────── */
window.addEventListener('message',function(e){
var d=e.data;
if(!d||d.source!==HOST||d.artboardId!==aid)return;
var m=d.message||{};var el;
if(m.type==='PATCH_ELEMENT_STYLE'){el=byId(m.nodeId);if(el)el.style.setProperty(m.property,m.value);}
else if(m.type==='REMOVE_ELEMENT'){el=byId(m.nodeId);if(el)el.style.display='none';}
else if(m.type==='REQUEST_ELEMENT_STYLES'){el=byId(m.nodeId);if(el)post({type:'ELEMENT_STYLES',nodeId:m.nodeId,styles:capStyles(el)});}
else if(m.type==='SELECT_COMPONENT'){el=byId(m.nodeId);selectEl(el||null);}
else if(m.type==='DESELECT'){selectEl(null);}
else if(m.type==='SET_DESIGN_TOKENS'){var tks=m.tokens||{},ks=Object.keys(tks);for(var i=0;i<ks.length;i++)document.documentElement.style.setProperty(ks[i],tks[ks[i]]);}
else if(m.type==='NAVIGATE'){if(m.path)window.location.pathname=m.path;}
});
/* ── Route discovery ────────────────────────── */
function discoverRoutes(){
var seen=Object.create(null),out=[];
var as=document.querySelectorAll('a[href]');
for(var i=0;i<as.length;i++){
try{
var url=new URL(as[i].href,window.location.href);
if(url.origin!==window.location.origin)continue;
var p=url.pathname;if(seen[p])continue;seen[p]=true;
var label=(as[i].textContent||'').trim().slice(0,40)||p;
out.push({path:p,label:label});
}catch(_){}
}
if(out.length)post({type:'ROUTES_DISCOVERED',routes:out});
}
/* ── Init ───────────────────────────────────── */
function init(){ensureCss();post({type:'READY'});discoverRoutes();}
if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',init);}else{init();}
})();`;
}
/**
* @deprecated Use buildDomInspectorScript() instead.
* Kept for backward compatibility — now returns the DOM inspector, not the fiber hook.
*/
export function buildProxyFiberHookScript(): string {
return buildDomInspectorScript();
}
+53 -11
View File
@@ -478,10 +478,13 @@ export function buildProxyFiberHookScript(): string {
}
// ── Route discovery ───────────────────────────────────────────────────────
// Scans <a href> links and the fiber nodeMap for same-origin routes, then
// posts ROUTES_DISCOVERED so the host canvas can auto-create artboards for
// every page. Called once 800 ms after READY (giving React time to paint)
// and again on every SPA popstate so navigation is reflected in the canvas.
// Mines routes from four sources (in priority order):
// 1. window.__NEXT_DATA__ (Next.js Pages Router build manifest)
// 2. window.__next_router_basepath (App Router)
// 3. <a href> same-origin anchor links in the rendered DOM
// 4. Fiber nodeMap — Link/NavLink/NextLink component props
// Called 800 ms after READY and on every SPA popstate navigation.
function humanLabel(path) {
var seg = path.replace(/\/+$/, '').split('/').filter(function(s) { return s.length > 0; });
if (seg.length === 0) return 'Home';
@@ -494,24 +497,58 @@ export function buildProxyFiberHookScript(): string {
var routes = [];
function addRoute(path, hint) {
if (!path || seen[path]) return;
if (path.charAt(0) === '#') return;
seen[path] = true;
var label = (hint && typeof hint === 'string' && hint.trim().slice(0, 50)) || humanLabel(path);
routes.push({ path: path, label: label });
if (!path || typeof path !== 'string') return;
// Skip hash fragments, external links that slipped through, and dynamic segments
if (path.charAt(0) !== '/') return;
// Normalise: strip trailing slash except for root
var norm = path.length > 1 ? path.replace(/\\/+$/, '') : '/';
if (seen[norm]) return;
seen[norm] = true;
var label = (hint && typeof hint === 'string' && hint.trim().slice(0, 50)) || humanLabel(norm);
routes.push({ path: norm, label: label });
}
// ① Current page is always included
addRoute(window.location.pathname, document.title || undefined);
// ② Next.js Pages Router — __NEXT_DATA__ contains the full page list in build id manifest
try {
var nextData = window.__NEXT_DATA__;
if (nextData && nextData.buildId) {
// Fetch the pages manifest — available at /_next/static/{buildId}/_buildManifest.js
var manifestUrl = '/_next/static/' + nextData.buildId + '/_buildManifest.js';
var xhr = new XMLHttpRequest();
xhr.open('GET', manifestUrl, false); // sync — runs at init time before user interaction
xhr.send();
if (xhr.status === 200) {
// Manifest exposes self.__BUILD_MANIFEST = { sortedPages: [...] }
var match = xhr.responseText.match(/sortedPages\\s*:\\s*(\\[[^\\]]+\\])/);
if (match) {
try {
var pages = JSON.parse(match[1]);
pages.forEach(function(p) {
// Skip catch-all and dynamic segments for now; static routes only
if (p.indexOf('[') === -1) addRoute(p);
});
} catch (e) { /* parse failed */ }
}
}
}
} catch (e) { /* Next.js not present or manifest unavailable */ }
// ③ <a href> same-origin links from the rendered DOM
var anchors = document.querySelectorAll('a[href]');
for (var i = 0; i < anchors.length; i++) {
try {
var url = new URL(anchors[i].href, window.location.href);
if (url.origin !== window.location.origin) continue;
addRoute(url.pathname, anchors[i].textContent || undefined);
// Skip hash-only links
if (!url.pathname || url.hash && !url.pathname) continue;
addRoute(url.pathname, (anchors[i].textContent || '').trim() || undefined);
} catch (e) { /* skip malformed hrefs */ }
}
// ④ Fiber nodeMap — Link/NavLink props carry href/to
Object.keys(nodeMap).forEach(function(nid) {
var entry = nodeMap[nid];
var fiber = entry && entry.fiber;
@@ -519,7 +556,11 @@ export function buildProxyFiberHookScript(): string {
if (name && /^(Link|NavLink|NextLink|RouterLink|a)$/.test(name)) {
var props = fiber.memoizedProps;
var href = props && (props.href || props.to);
if (typeof href === 'string' && href.charAt(0) === '/') addRoute(href);
if (typeof href === 'string') addRoute(href);
// Next.js <Link href={{ pathname }}>
if (href && typeof href === 'object' && typeof href.pathname === 'string') {
addRoute(href.pathname);
}
}
});
@@ -529,6 +570,7 @@ export function buildProxyFiberHookScript(): string {
// ── Ready signal ──────────────────────────────────────────────────────────
post({ type: 'READY' });
setTimeout(discoverRoutes, 800);
// Re-discover on SPA navigation (Next.js App Router fires popstate on push)
window.addEventListener('popstate', function() { setTimeout(discoverRoutes, 100); });
})();`;
}
+4
View File
@@ -16,7 +16,11 @@ export type {
RendererEnvelope,
} from './protocol.js';
// React fiber hook — hooks into __REACT_DEVTOOLS_GLOBAL_HOOK__ to intercept
// React commits. Works only in React apps loaded in an Originmain iframe.
export { buildFiberHookScript, buildProxyFiberHookScript } from './fiber-hook.js';
// Universal DOM inspector — fallback for static HTML pages (React not required)
export { buildDomInspectorScript } from './dom-inspector.js';
export {
createRendererHostConfig,
+1 -1
View File
@@ -48,7 +48,7 @@ export interface HostEnvelope {
export type RendererMessage =
| { type: 'READY' }
| { type: 'FIBER_TREE_UPDATE'; root: FiberNode }
| { type: 'COMPONENT_SELECTED'; nodeId: string; rect: DOMRectLike }
| { type: 'COMPONENT_SELECTED'; nodeId: string; nodeName?: string; rect: DOMRectLike }
| { type: 'COMPONENT_DESELECTED' }
| { type: 'ERROR'; message: string }
/** Response to REQUEST_ELEMENT_STYLES — computed CSS properties for the node. */