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.
@@ -1,4 +1,4 @@
1
- /*! Morph Embed SDK v0.2.0 — https://usemorph.xyz — MIT */
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
- mount(h) { handlers = h || {}; if (!host) build(); },
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.mount({ onSubmit, onUndo });
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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, extractContext: window.Morph.extractContext,
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();
@@ -1,4 +1,4 @@
1
- /*! Morph Embed SDK v0.2.0 — https://usemorph.xyz — MIT */
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
- mount(h) { handlers = h || {}; if (!host) build(); },
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.mount({ onSubmit, onUndo });
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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, extractContext: window.Morph.extractContext,
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.0",
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",