morph-embed 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/morph-embed.esm.mjs +213 -4
- package/dist/morph-embed.js +213 -4
- package/package.json +1 -1
package/dist/morph-embed.esm.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Morph Embed SDK v0.2.
|
|
1
|
+
/*! Morph Embed SDK v0.2.1 — https://usemorph.xyz — MIT */
|
|
2
2
|
const g = typeof window !== "undefined" ? window : globalThis;
|
|
3
3
|
|
|
4
4
|
/* ---- shared/dsl.js ---- */
|
|
@@ -2270,7 +2270,9 @@ const g = typeof window !== "undefined" ? window : globalThis;
|
|
|
2270
2270
|
function aimActive(on) { if (els && els.aim) els.aim.classList.toggle('on', !!on); }
|
|
2271
2271
|
|
|
2272
2272
|
window.Morph.bar = {
|
|
2273
|
-
|
|
2273
|
+
// No onPick handler -> no picker in this embedding: hide the ⌖ button so
|
|
2274
|
+
// users never see a dead control (the extension always passes onPick).
|
|
2275
|
+
mount(h) { handlers = h || {}; if (!host) build(); if (els && els.aim) els.aim.style.display = handlers.onPick ? '' : 'none'; },
|
|
2274
2276
|
show, hide, toggle,
|
|
2275
2277
|
setStatus, clearStatus, setBusy, showActions,
|
|
2276
2278
|
isVisible: () => visible,
|
|
@@ -2290,6 +2292,152 @@ const g = typeof window !== "undefined" ? window : globalThis;
|
|
|
2290
2292
|
})();
|
|
2291
2293
|
|
|
2292
2294
|
|
|
2295
|
+
/* ---- extension/src/picker.js ---- */
|
|
2296
|
+
/*
|
|
2297
|
+
* Element picker — lets the user click a specific element on the page so the next
|
|
2298
|
+
* reshape is scoped to it (and its descendants) instead of the model guessing
|
|
2299
|
+
* selectors across the whole page.
|
|
2300
|
+
*
|
|
2301
|
+
* start(onPick, onCancel) : enter pick mode (hover highlights, click selects, Esc cancels)
|
|
2302
|
+
* stop() : leave pick mode
|
|
2303
|
+
* selectorFor(el, doc) : PURE — a stable-ish unique CSS selector for el
|
|
2304
|
+
* describe(el) : PURE — a short human label for el
|
|
2305
|
+
*
|
|
2306
|
+
* Attaches to window.Morph.picker. Pure helpers are CJS-exported for tests.
|
|
2307
|
+
*/
|
|
2308
|
+
(function () {
|
|
2309
|
+
'use strict';
|
|
2310
|
+
window.Morph = window.Morph || {};
|
|
2311
|
+
|
|
2312
|
+
// Prefer the spec-correct native CSS.escape (all modern browsers have it). Fall
|
|
2313
|
+
// back (e.g. jsdom / very old engines) to a regex that backslash-escapes any
|
|
2314
|
+
// non-identifier char AND escapes a leading digit per the CSS spec — otherwise
|
|
2315
|
+
// an id like "2col" yields the INVALID selector "#2col", which throws at
|
|
2316
|
+
// querySelectorAll and forces a weaker, possibly non-unique path.
|
|
2317
|
+
function cssEscape(s) {
|
|
2318
|
+
s = String(s);
|
|
2319
|
+
if (typeof window !== 'undefined' && window.CSS && typeof window.CSS.escape === 'function') {
|
|
2320
|
+
return window.CSS.escape(s);
|
|
2321
|
+
}
|
|
2322
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, (c) => '\\' + c).replace(/^([0-9])/, (m, d) => '\\3' + d + ' ');
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// A reasonably-unique selector: prefer a unique #id (anchoring the path there if
|
|
2326
|
+
// an ancestor has one), else a short `>`-path with :nth-of-type disambiguation.
|
|
2327
|
+
function selectorFor(el, doc) {
|
|
2328
|
+
doc = doc || (el && el.ownerDocument) || (typeof document !== 'undefined' ? document : null);
|
|
2329
|
+
if (!el || el.nodeType !== 1 || !doc) return '';
|
|
2330
|
+
if (el.id) {
|
|
2331
|
+
const sel = '#' + cssEscape(el.id);
|
|
2332
|
+
try { if (doc.querySelectorAll(sel).length === 1) return sel; } catch (_) { /* keep going */ }
|
|
2333
|
+
}
|
|
2334
|
+
const parts = [];
|
|
2335
|
+
let node = el;
|
|
2336
|
+
let depth = 0;
|
|
2337
|
+
while (node && node.nodeType === 1 && node !== doc.body && node !== doc.documentElement && depth < 6) {
|
|
2338
|
+
if (node.id) {
|
|
2339
|
+
const idSel = '#' + cssEscape(node.id);
|
|
2340
|
+
let unique = false;
|
|
2341
|
+
try { unique = doc.querySelectorAll(idSel).length === 1; } catch (_) { unique = false; }
|
|
2342
|
+
if (unique) { parts.unshift(idSel); return parts.join(' > '); }
|
|
2343
|
+
}
|
|
2344
|
+
let part = node.tagName.toLowerCase();
|
|
2345
|
+
const parent = node.parentNode;
|
|
2346
|
+
if (parent && parent.children) {
|
|
2347
|
+
const sameTag = Array.prototype.filter.call(parent.children, (c) => c.tagName === node.tagName);
|
|
2348
|
+
if (sameTag.length > 1) part += ':nth-of-type(' + (sameTag.indexOf(node) + 1) + ')';
|
|
2349
|
+
}
|
|
2350
|
+
parts.unshift(part);
|
|
2351
|
+
node = node.parentNode;
|
|
2352
|
+
depth++;
|
|
2353
|
+
}
|
|
2354
|
+
return parts.join(' > ');
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
function describe(el) {
|
|
2358
|
+
if (!el || el.nodeType !== 1) return 'element';
|
|
2359
|
+
let s = el.tagName.toLowerCase();
|
|
2360
|
+
if (el.id) s += '#' + el.id;
|
|
2361
|
+
else if (el.classList && el.classList.length) s += '.' + el.classList[0];
|
|
2362
|
+
const text = (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 24);
|
|
2363
|
+
if (text) s += ' “' + text + '”';
|
|
2364
|
+
return s;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// --- interactive pick mode (needs a live DOM) -----------------------------
|
|
2368
|
+
let active = false, overlay = null, current = null, pickCb = null, cancelCb = null;
|
|
2369
|
+
|
|
2370
|
+
function ensureOverlay() {
|
|
2371
|
+
if (overlay) return overlay;
|
|
2372
|
+
overlay = document.createElement('div');
|
|
2373
|
+
overlay.setAttribute('data-morph-pick-overlay', '1');
|
|
2374
|
+
const s = overlay.style;
|
|
2375
|
+
s.position = 'fixed'; s.zIndex = '2147483646'; s.pointerEvents = 'none';
|
|
2376
|
+
s.border = '2px solid #ffffff'; s.boxShadow = '0 0 0 1px #000, 0 0 0 4px rgba(255,255,255,0.25)';
|
|
2377
|
+
s.background = 'rgba(255,255,255,0.06)'; s.borderRadius = '2px'; s.display = 'none';
|
|
2378
|
+
s.transition = 'left .04s linear, top .04s linear, width .04s linear, height .04s linear';
|
|
2379
|
+
return overlay;
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// Never let the picker target Morph's own UI (the bar host or this overlay).
|
|
2383
|
+
function isOurs(el) {
|
|
2384
|
+
try { return !!(el && el.closest && el.closest('#morph-host-root, [data-morph-pick-overlay]')); }
|
|
2385
|
+
catch (_) { return false; }
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function moveOverlay(el) {
|
|
2389
|
+
const r = el.getBoundingClientRect();
|
|
2390
|
+
const s = overlay.style;
|
|
2391
|
+
s.display = 'block';
|
|
2392
|
+
s.left = r.left + 'px'; s.top = r.top + 'px'; s.width = r.width + 'px'; s.height = r.height + 'px';
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
function onMove(e) {
|
|
2396
|
+
const el = e.target;
|
|
2397
|
+
if (!el || isOurs(el)) { overlay.style.display = 'none'; current = null; return; }
|
|
2398
|
+
current = el;
|
|
2399
|
+
try { moveOverlay(el); } catch (_) { /* never break the page */ }
|
|
2400
|
+
}
|
|
2401
|
+
function onClick(e) {
|
|
2402
|
+
if (isOurs(e.target)) return; // let our own UI receive clicks
|
|
2403
|
+
e.preventDefault(); e.stopPropagation();
|
|
2404
|
+
finish(current || e.target);
|
|
2405
|
+
}
|
|
2406
|
+
function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); cancel(); } }
|
|
2407
|
+
|
|
2408
|
+
function teardown() {
|
|
2409
|
+
active = false; current = null;
|
|
2410
|
+
try {
|
|
2411
|
+
document.removeEventListener('mousemove', onMove, true);
|
|
2412
|
+
document.removeEventListener('click', onClick, true);
|
|
2413
|
+
document.removeEventListener('keydown', onKey, true);
|
|
2414
|
+
if (document.documentElement) document.documentElement.style.cursor = '';
|
|
2415
|
+
if (overlay) overlay.style.display = 'none';
|
|
2416
|
+
} catch (_) { /* noop */ }
|
|
2417
|
+
}
|
|
2418
|
+
function finish(el) { const cb = pickCb; teardown(); if (cb) { try { cb(el); } catch (_) {} } }
|
|
2419
|
+
function cancel() { const cb = cancelCb; teardown(); if (cb) { try { cb(); } catch (_) {} } }
|
|
2420
|
+
|
|
2421
|
+
function start(onPick, onCancel) {
|
|
2422
|
+
if (active) return;
|
|
2423
|
+
active = true; pickCb = onPick; cancelCb = onCancel;
|
|
2424
|
+
try {
|
|
2425
|
+
ensureOverlay();
|
|
2426
|
+
(document.body || document.documentElement).appendChild(overlay);
|
|
2427
|
+
document.addEventListener('mousemove', onMove, true);
|
|
2428
|
+
document.addEventListener('click', onClick, true);
|
|
2429
|
+
document.addEventListener('keydown', onKey, true);
|
|
2430
|
+
if (document.documentElement) document.documentElement.style.cursor = 'crosshair';
|
|
2431
|
+
} catch (_) { teardown(); }
|
|
2432
|
+
}
|
|
2433
|
+
function stop() { teardown(); }
|
|
2434
|
+
function isActive() { return active; }
|
|
2435
|
+
|
|
2436
|
+
window.Morph.picker = { start, stop, isActive, selectorFor, describe };
|
|
2437
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = { selectorFor, describe };
|
|
2438
|
+
})();
|
|
2439
|
+
|
|
2440
|
+
|
|
2293
2441
|
/* ---- sdk/src/ui.js ---- */
|
|
2294
2442
|
/*
|
|
2295
2443
|
* Morph SDK — UI layer: launcher + edits panel + undo toast, isolated in a
|
|
@@ -2667,6 +2815,7 @@ const g = typeof window !== "undefined" ? window : globalThis;
|
|
|
2667
2815
|
const store = deps.store;
|
|
2668
2816
|
const bar = deps.bar;
|
|
2669
2817
|
const ui = deps.ui || null;
|
|
2818
|
+
const picker = deps.picker || null;
|
|
2670
2819
|
const extractContext = deps.extractContext;
|
|
2671
2820
|
const fetchFn = deps.fetchFn;
|
|
2672
2821
|
const stageDelays = deps.stageDelays || STAGE_DELAYS;
|
|
@@ -2700,10 +2849,68 @@ const g = typeof window !== "undefined" ? window : globalThis;
|
|
|
2700
2849
|
}
|
|
2701
2850
|
backend = (typeof config.backend === 'string' && config.backend)
|
|
2702
2851
|
? config.backend.replace(/\/$/, '') : DEFAULT_BACKEND;
|
|
2703
|
-
bar
|
|
2852
|
+
// onPick only when a picker exists — the bar hides the ⌖ button when
|
|
2853
|
+
// no handler is passed, so users never see a dead control.
|
|
2854
|
+
const handlers = { onSubmit, onUndo, onHide };
|
|
2855
|
+
if (picker) handlers.onPick = onPick;
|
|
2856
|
+
bar.mount(handlers);
|
|
2704
2857
|
return api;
|
|
2705
2858
|
}
|
|
2706
2859
|
|
|
2860
|
+
// ---- element targeting (mirrors extension/src/content.js) --------------
|
|
2861
|
+
let activeTarget = null; // { selector, label } while the user has a pick
|
|
2862
|
+
|
|
2863
|
+
function escapeHtml(s) {
|
|
2864
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
2865
|
+
}
|
|
2866
|
+
function outlineTarget(el) {
|
|
2867
|
+
try {
|
|
2868
|
+
if (!win.document.getElementById('morph-sdk-target-style')) {
|
|
2869
|
+
const st = win.document.createElement('style');
|
|
2870
|
+
st.id = 'morph-sdk-target-style';
|
|
2871
|
+
st.textContent = '[data-morph-targeted="1"]{outline:2px solid #4c8dff !important;outline-offset:2px !important;}';
|
|
2872
|
+
win.document.documentElement.appendChild(st);
|
|
2873
|
+
}
|
|
2874
|
+
el.setAttribute('data-morph-targeted', '1');
|
|
2875
|
+
} catch (_) { /* cosmetic only */ }
|
|
2876
|
+
}
|
|
2877
|
+
function clearTargetState() {
|
|
2878
|
+
activeTarget = null;
|
|
2879
|
+
try {
|
|
2880
|
+
const el = win.document.querySelector('[data-morph-targeted="1"]');
|
|
2881
|
+
if (el) el.removeAttribute('data-morph-targeted');
|
|
2882
|
+
} catch (_) { /* noop */ }
|
|
2883
|
+
try { bar.clearTarget(); } catch (_) { /* bar optional */ }
|
|
2884
|
+
}
|
|
2885
|
+
function targetHtml() {
|
|
2886
|
+
try {
|
|
2887
|
+
const el = activeTarget && win.document.querySelector(activeTarget.selector);
|
|
2888
|
+
return el ? String(el.outerHTML).slice(0, 4000) : '';
|
|
2889
|
+
} catch (_) { return ''; }
|
|
2890
|
+
}
|
|
2891
|
+
function onPick() {
|
|
2892
|
+
if (!picker || !picker.start) return;
|
|
2893
|
+
try { bar.setStatus('Click any element on the page to target it — Esc to cancel.', ''); } catch (_) { /* noop */ }
|
|
2894
|
+
picker.start((el) => {
|
|
2895
|
+
try {
|
|
2896
|
+
const selector = picker.selectorFor(el, win.document);
|
|
2897
|
+
if (!selector) { bar.setStatus('Couldn’t target that element — try another.', 'err'); return; }
|
|
2898
|
+
activeTarget = { selector, label: picker.describe(el) };
|
|
2899
|
+
outlineTarget(el);
|
|
2900
|
+
bar.setTarget({ label: activeTarget.label, onClear: clearTargetState });
|
|
2901
|
+
bar.setStatus('Targeting <b>' + escapeHtml(activeTarget.label) + '</b>. Now type how to change it.', 'ok');
|
|
2902
|
+
if (bar.focusInput) bar.focusInput();
|
|
2903
|
+
} catch (_) { /* never break the page */ }
|
|
2904
|
+
}, () => {
|
|
2905
|
+
try { bar.aimActive(false); bar.setStatus('Pick cancelled.', ''); } catch (_) { /* noop */ }
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
// Closing the bar ends targeting: cancel any in-progress pick, drop the chip.
|
|
2909
|
+
function onHide() {
|
|
2910
|
+
try { if (picker && picker.stop) picker.stop(); } catch (_) { /* noop */ }
|
|
2911
|
+
clearTargetState();
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2707
2914
|
// Confirm gate for actions. window.confirm is trusted browser chrome the
|
|
2708
2915
|
// page can't spoof; a bar-styled dialog can replace it later.
|
|
2709
2916
|
function confirmFn(name, description) {
|
|
@@ -2798,6 +3005,7 @@ const g = typeof window !== "undefined" ? window : globalThis;
|
|
|
2798
3005
|
let resp;
|
|
2799
3006
|
try {
|
|
2800
3007
|
const body = { prompt, context, url: win.location.href };
|
|
3008
|
+
if (activeTarget) body.target = { selector: activeTarget.selector, html: targetHtml() };
|
|
2801
3009
|
if (registry.hasSources() || registry.hasActions()) body.manifests = registry.manifests();
|
|
2802
3010
|
const r = await fetchFn(backend + '/api/transform', {
|
|
2803
3011
|
method: 'POST',
|
|
@@ -2966,7 +3174,8 @@ const g = typeof window !== "undefined" ? window : globalThis;
|
|
|
2966
3174
|
});
|
|
2967
3175
|
const embed = createEmbed({
|
|
2968
3176
|
config, win: window, DSL: window.MorphDSL, registry, store,
|
|
2969
|
-
bar: window.Morph.bar, ui,
|
|
3177
|
+
bar: window.Morph.bar, ui, picker: window.Morph.picker,
|
|
3178
|
+
extractContext: window.Morph.extractContext,
|
|
2970
3179
|
fetchFn: doFetch, compose: window.MorphStorage.composeEdits,
|
|
2971
3180
|
});
|
|
2972
3181
|
embed.init();
|
package/dist/morph-embed.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Morph Embed SDK v0.2.
|
|
1
|
+
/*! Morph Embed SDK v0.2.1 — https://usemorph.xyz — MIT */
|
|
2
2
|
|
|
3
3
|
/* ---- shared/dsl.js ---- */
|
|
4
4
|
/*
|
|
@@ -2269,7 +2269,9 @@
|
|
|
2269
2269
|
function aimActive(on) { if (els && els.aim) els.aim.classList.toggle('on', !!on); }
|
|
2270
2270
|
|
|
2271
2271
|
window.Morph.bar = {
|
|
2272
|
-
|
|
2272
|
+
// No onPick handler -> no picker in this embedding: hide the ⌖ button so
|
|
2273
|
+
// users never see a dead control (the extension always passes onPick).
|
|
2274
|
+
mount(h) { handlers = h || {}; if (!host) build(); if (els && els.aim) els.aim.style.display = handlers.onPick ? '' : 'none'; },
|
|
2273
2275
|
show, hide, toggle,
|
|
2274
2276
|
setStatus, clearStatus, setBusy, showActions,
|
|
2275
2277
|
isVisible: () => visible,
|
|
@@ -2289,6 +2291,152 @@
|
|
|
2289
2291
|
})();
|
|
2290
2292
|
|
|
2291
2293
|
|
|
2294
|
+
/* ---- extension/src/picker.js ---- */
|
|
2295
|
+
/*
|
|
2296
|
+
* Element picker — lets the user click a specific element on the page so the next
|
|
2297
|
+
* reshape is scoped to it (and its descendants) instead of the model guessing
|
|
2298
|
+
* selectors across the whole page.
|
|
2299
|
+
*
|
|
2300
|
+
* start(onPick, onCancel) : enter pick mode (hover highlights, click selects, Esc cancels)
|
|
2301
|
+
* stop() : leave pick mode
|
|
2302
|
+
* selectorFor(el, doc) : PURE — a stable-ish unique CSS selector for el
|
|
2303
|
+
* describe(el) : PURE — a short human label for el
|
|
2304
|
+
*
|
|
2305
|
+
* Attaches to window.Morph.picker. Pure helpers are CJS-exported for tests.
|
|
2306
|
+
*/
|
|
2307
|
+
(function () {
|
|
2308
|
+
'use strict';
|
|
2309
|
+
window.Morph = window.Morph || {};
|
|
2310
|
+
|
|
2311
|
+
// Prefer the spec-correct native CSS.escape (all modern browsers have it). Fall
|
|
2312
|
+
// back (e.g. jsdom / very old engines) to a regex that backslash-escapes any
|
|
2313
|
+
// non-identifier char AND escapes a leading digit per the CSS spec — otherwise
|
|
2314
|
+
// an id like "2col" yields the INVALID selector "#2col", which throws at
|
|
2315
|
+
// querySelectorAll and forces a weaker, possibly non-unique path.
|
|
2316
|
+
function cssEscape(s) {
|
|
2317
|
+
s = String(s);
|
|
2318
|
+
if (typeof window !== 'undefined' && window.CSS && typeof window.CSS.escape === 'function') {
|
|
2319
|
+
return window.CSS.escape(s);
|
|
2320
|
+
}
|
|
2321
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, (c) => '\\' + c).replace(/^([0-9])/, (m, d) => '\\3' + d + ' ');
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
// A reasonably-unique selector: prefer a unique #id (anchoring the path there if
|
|
2325
|
+
// an ancestor has one), else a short `>`-path with :nth-of-type disambiguation.
|
|
2326
|
+
function selectorFor(el, doc) {
|
|
2327
|
+
doc = doc || (el && el.ownerDocument) || (typeof document !== 'undefined' ? document : null);
|
|
2328
|
+
if (!el || el.nodeType !== 1 || !doc) return '';
|
|
2329
|
+
if (el.id) {
|
|
2330
|
+
const sel = '#' + cssEscape(el.id);
|
|
2331
|
+
try { if (doc.querySelectorAll(sel).length === 1) return sel; } catch (_) { /* keep going */ }
|
|
2332
|
+
}
|
|
2333
|
+
const parts = [];
|
|
2334
|
+
let node = el;
|
|
2335
|
+
let depth = 0;
|
|
2336
|
+
while (node && node.nodeType === 1 && node !== doc.body && node !== doc.documentElement && depth < 6) {
|
|
2337
|
+
if (node.id) {
|
|
2338
|
+
const idSel = '#' + cssEscape(node.id);
|
|
2339
|
+
let unique = false;
|
|
2340
|
+
try { unique = doc.querySelectorAll(idSel).length === 1; } catch (_) { unique = false; }
|
|
2341
|
+
if (unique) { parts.unshift(idSel); return parts.join(' > '); }
|
|
2342
|
+
}
|
|
2343
|
+
let part = node.tagName.toLowerCase();
|
|
2344
|
+
const parent = node.parentNode;
|
|
2345
|
+
if (parent && parent.children) {
|
|
2346
|
+
const sameTag = Array.prototype.filter.call(parent.children, (c) => c.tagName === node.tagName);
|
|
2347
|
+
if (sameTag.length > 1) part += ':nth-of-type(' + (sameTag.indexOf(node) + 1) + ')';
|
|
2348
|
+
}
|
|
2349
|
+
parts.unshift(part);
|
|
2350
|
+
node = node.parentNode;
|
|
2351
|
+
depth++;
|
|
2352
|
+
}
|
|
2353
|
+
return parts.join(' > ');
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
function describe(el) {
|
|
2357
|
+
if (!el || el.nodeType !== 1) return 'element';
|
|
2358
|
+
let s = el.tagName.toLowerCase();
|
|
2359
|
+
if (el.id) s += '#' + el.id;
|
|
2360
|
+
else if (el.classList && el.classList.length) s += '.' + el.classList[0];
|
|
2361
|
+
const text = (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 24);
|
|
2362
|
+
if (text) s += ' “' + text + '”';
|
|
2363
|
+
return s;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// --- interactive pick mode (needs a live DOM) -----------------------------
|
|
2367
|
+
let active = false, overlay = null, current = null, pickCb = null, cancelCb = null;
|
|
2368
|
+
|
|
2369
|
+
function ensureOverlay() {
|
|
2370
|
+
if (overlay) return overlay;
|
|
2371
|
+
overlay = document.createElement('div');
|
|
2372
|
+
overlay.setAttribute('data-morph-pick-overlay', '1');
|
|
2373
|
+
const s = overlay.style;
|
|
2374
|
+
s.position = 'fixed'; s.zIndex = '2147483646'; s.pointerEvents = 'none';
|
|
2375
|
+
s.border = '2px solid #ffffff'; s.boxShadow = '0 0 0 1px #000, 0 0 0 4px rgba(255,255,255,0.25)';
|
|
2376
|
+
s.background = 'rgba(255,255,255,0.06)'; s.borderRadius = '2px'; s.display = 'none';
|
|
2377
|
+
s.transition = 'left .04s linear, top .04s linear, width .04s linear, height .04s linear';
|
|
2378
|
+
return overlay;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// Never let the picker target Morph's own UI (the bar host or this overlay).
|
|
2382
|
+
function isOurs(el) {
|
|
2383
|
+
try { return !!(el && el.closest && el.closest('#morph-host-root, [data-morph-pick-overlay]')); }
|
|
2384
|
+
catch (_) { return false; }
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
function moveOverlay(el) {
|
|
2388
|
+
const r = el.getBoundingClientRect();
|
|
2389
|
+
const s = overlay.style;
|
|
2390
|
+
s.display = 'block';
|
|
2391
|
+
s.left = r.left + 'px'; s.top = r.top + 'px'; s.width = r.width + 'px'; s.height = r.height + 'px';
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
function onMove(e) {
|
|
2395
|
+
const el = e.target;
|
|
2396
|
+
if (!el || isOurs(el)) { overlay.style.display = 'none'; current = null; return; }
|
|
2397
|
+
current = el;
|
|
2398
|
+
try { moveOverlay(el); } catch (_) { /* never break the page */ }
|
|
2399
|
+
}
|
|
2400
|
+
function onClick(e) {
|
|
2401
|
+
if (isOurs(e.target)) return; // let our own UI receive clicks
|
|
2402
|
+
e.preventDefault(); e.stopPropagation();
|
|
2403
|
+
finish(current || e.target);
|
|
2404
|
+
}
|
|
2405
|
+
function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); cancel(); } }
|
|
2406
|
+
|
|
2407
|
+
function teardown() {
|
|
2408
|
+
active = false; current = null;
|
|
2409
|
+
try {
|
|
2410
|
+
document.removeEventListener('mousemove', onMove, true);
|
|
2411
|
+
document.removeEventListener('click', onClick, true);
|
|
2412
|
+
document.removeEventListener('keydown', onKey, true);
|
|
2413
|
+
if (document.documentElement) document.documentElement.style.cursor = '';
|
|
2414
|
+
if (overlay) overlay.style.display = 'none';
|
|
2415
|
+
} catch (_) { /* noop */ }
|
|
2416
|
+
}
|
|
2417
|
+
function finish(el) { const cb = pickCb; teardown(); if (cb) { try { cb(el); } catch (_) {} } }
|
|
2418
|
+
function cancel() { const cb = cancelCb; teardown(); if (cb) { try { cb(); } catch (_) {} } }
|
|
2419
|
+
|
|
2420
|
+
function start(onPick, onCancel) {
|
|
2421
|
+
if (active) return;
|
|
2422
|
+
active = true; pickCb = onPick; cancelCb = onCancel;
|
|
2423
|
+
try {
|
|
2424
|
+
ensureOverlay();
|
|
2425
|
+
(document.body || document.documentElement).appendChild(overlay);
|
|
2426
|
+
document.addEventListener('mousemove', onMove, true);
|
|
2427
|
+
document.addEventListener('click', onClick, true);
|
|
2428
|
+
document.addEventListener('keydown', onKey, true);
|
|
2429
|
+
if (document.documentElement) document.documentElement.style.cursor = 'crosshair';
|
|
2430
|
+
} catch (_) { teardown(); }
|
|
2431
|
+
}
|
|
2432
|
+
function stop() { teardown(); }
|
|
2433
|
+
function isActive() { return active; }
|
|
2434
|
+
|
|
2435
|
+
window.Morph.picker = { start, stop, isActive, selectorFor, describe };
|
|
2436
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = { selectorFor, describe };
|
|
2437
|
+
})();
|
|
2438
|
+
|
|
2439
|
+
|
|
2292
2440
|
/* ---- sdk/src/ui.js ---- */
|
|
2293
2441
|
/*
|
|
2294
2442
|
* Morph SDK — UI layer: launcher + edits panel + undo toast, isolated in a
|
|
@@ -2666,6 +2814,7 @@
|
|
|
2666
2814
|
const store = deps.store;
|
|
2667
2815
|
const bar = deps.bar;
|
|
2668
2816
|
const ui = deps.ui || null;
|
|
2817
|
+
const picker = deps.picker || null;
|
|
2669
2818
|
const extractContext = deps.extractContext;
|
|
2670
2819
|
const fetchFn = deps.fetchFn;
|
|
2671
2820
|
const stageDelays = deps.stageDelays || STAGE_DELAYS;
|
|
@@ -2699,10 +2848,68 @@
|
|
|
2699
2848
|
}
|
|
2700
2849
|
backend = (typeof config.backend === 'string' && config.backend)
|
|
2701
2850
|
? config.backend.replace(/\/$/, '') : DEFAULT_BACKEND;
|
|
2702
|
-
bar
|
|
2851
|
+
// onPick only when a picker exists — the bar hides the ⌖ button when
|
|
2852
|
+
// no handler is passed, so users never see a dead control.
|
|
2853
|
+
const handlers = { onSubmit, onUndo, onHide };
|
|
2854
|
+
if (picker) handlers.onPick = onPick;
|
|
2855
|
+
bar.mount(handlers);
|
|
2703
2856
|
return api;
|
|
2704
2857
|
}
|
|
2705
2858
|
|
|
2859
|
+
// ---- element targeting (mirrors extension/src/content.js) --------------
|
|
2860
|
+
let activeTarget = null; // { selector, label } while the user has a pick
|
|
2861
|
+
|
|
2862
|
+
function escapeHtml(s) {
|
|
2863
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
2864
|
+
}
|
|
2865
|
+
function outlineTarget(el) {
|
|
2866
|
+
try {
|
|
2867
|
+
if (!win.document.getElementById('morph-sdk-target-style')) {
|
|
2868
|
+
const st = win.document.createElement('style');
|
|
2869
|
+
st.id = 'morph-sdk-target-style';
|
|
2870
|
+
st.textContent = '[data-morph-targeted="1"]{outline:2px solid #4c8dff !important;outline-offset:2px !important;}';
|
|
2871
|
+
win.document.documentElement.appendChild(st);
|
|
2872
|
+
}
|
|
2873
|
+
el.setAttribute('data-morph-targeted', '1');
|
|
2874
|
+
} catch (_) { /* cosmetic only */ }
|
|
2875
|
+
}
|
|
2876
|
+
function clearTargetState() {
|
|
2877
|
+
activeTarget = null;
|
|
2878
|
+
try {
|
|
2879
|
+
const el = win.document.querySelector('[data-morph-targeted="1"]');
|
|
2880
|
+
if (el) el.removeAttribute('data-morph-targeted');
|
|
2881
|
+
} catch (_) { /* noop */ }
|
|
2882
|
+
try { bar.clearTarget(); } catch (_) { /* bar optional */ }
|
|
2883
|
+
}
|
|
2884
|
+
function targetHtml() {
|
|
2885
|
+
try {
|
|
2886
|
+
const el = activeTarget && win.document.querySelector(activeTarget.selector);
|
|
2887
|
+
return el ? String(el.outerHTML).slice(0, 4000) : '';
|
|
2888
|
+
} catch (_) { return ''; }
|
|
2889
|
+
}
|
|
2890
|
+
function onPick() {
|
|
2891
|
+
if (!picker || !picker.start) return;
|
|
2892
|
+
try { bar.setStatus('Click any element on the page to target it — Esc to cancel.', ''); } catch (_) { /* noop */ }
|
|
2893
|
+
picker.start((el) => {
|
|
2894
|
+
try {
|
|
2895
|
+
const selector = picker.selectorFor(el, win.document);
|
|
2896
|
+
if (!selector) { bar.setStatus('Couldn’t target that element — try another.', 'err'); return; }
|
|
2897
|
+
activeTarget = { selector, label: picker.describe(el) };
|
|
2898
|
+
outlineTarget(el);
|
|
2899
|
+
bar.setTarget({ label: activeTarget.label, onClear: clearTargetState });
|
|
2900
|
+
bar.setStatus('Targeting <b>' + escapeHtml(activeTarget.label) + '</b>. Now type how to change it.', 'ok');
|
|
2901
|
+
if (bar.focusInput) bar.focusInput();
|
|
2902
|
+
} catch (_) { /* never break the page */ }
|
|
2903
|
+
}, () => {
|
|
2904
|
+
try { bar.aimActive(false); bar.setStatus('Pick cancelled.', ''); } catch (_) { /* noop */ }
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
// Closing the bar ends targeting: cancel any in-progress pick, drop the chip.
|
|
2908
|
+
function onHide() {
|
|
2909
|
+
try { if (picker && picker.stop) picker.stop(); } catch (_) { /* noop */ }
|
|
2910
|
+
clearTargetState();
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2706
2913
|
// Confirm gate for actions. window.confirm is trusted browser chrome the
|
|
2707
2914
|
// page can't spoof; a bar-styled dialog can replace it later.
|
|
2708
2915
|
function confirmFn(name, description) {
|
|
@@ -2797,6 +3004,7 @@
|
|
|
2797
3004
|
let resp;
|
|
2798
3005
|
try {
|
|
2799
3006
|
const body = { prompt, context, url: win.location.href };
|
|
3007
|
+
if (activeTarget) body.target = { selector: activeTarget.selector, html: targetHtml() };
|
|
2800
3008
|
if (registry.hasSources() || registry.hasActions()) body.manifests = registry.manifests();
|
|
2801
3009
|
const r = await fetchFn(backend + '/api/transform', {
|
|
2802
3010
|
method: 'POST',
|
|
@@ -2965,7 +3173,8 @@
|
|
|
2965
3173
|
});
|
|
2966
3174
|
const embed = createEmbed({
|
|
2967
3175
|
config, win: window, DSL: window.MorphDSL, registry, store,
|
|
2968
|
-
bar: window.Morph.bar, ui,
|
|
3176
|
+
bar: window.Morph.bar, ui, picker: window.Morph.picker,
|
|
3177
|
+
extractContext: window.Morph.extractContext,
|
|
2969
3178
|
fetchFn: doFetch, compose: window.MorphStorage.composeEdits,
|
|
2970
3179
|
});
|
|
2971
3180
|
embed.init();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morph-embed",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Embed a prompt bar in your product so users can generate the features you haven't built — real-data widgets, layout reshapes, and confirm-gated actions. Declarative-DSL security model; your data never leaves the page.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/morph-embed.esm.mjs",
|