mnfst 0.5.232 → 0.5.234

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.232';
5
+ const MANIFEST_BUILD_VERSION = '0.5.234';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
package/lib/manifest.d.ts CHANGED
@@ -433,11 +433,13 @@ export interface ManifestEdit {
433
433
  /** Set an attribute: a string sets it, `true` sets it empty, `null`/`false` removes it. */
434
434
  attr(name: string, value: string | boolean | null, el?: Element): boolean;
435
435
  classes: ManifestEditClasses;
436
- /** Component scope of the target's area: read with no value, set 'instance' or 'main'.
436
+ /** Component scope of `el`'s area (default `$edit.target`): `scope(el)` reads it.
437
437
  * Instance overrides apply to an area that wraps exactly one component with no x-edit region
438
438
  * of its own; a component among other content is a host piece of a static area instead, and
439
439
  * a component's own region is edited in its own file. */
440
- scope(value?: 'instance' | 'main', el?: Element): 'instance' | 'main' | null | boolean;
440
+ scope(el?: Element | null): 'instance' | 'main' | null;
441
+ /** Set the component scope of `el`'s area (default `$edit.target`); false if it has none. */
442
+ scope(value: 'instance' | 'main', el?: Element | null): boolean;
441
443
  /** Revert a component element's instance overrides (the area element reverts all). */
442
444
  revert(el?: Element): boolean;
443
445
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.232';
5
+ const MANIFEST_BUILD_VERSION = '0.5.234';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -186,6 +186,14 @@
186
186
  }
187
187
  const sanitizeFor = (el, html) => !window.ManifestRichText ? sanitizeHTML(html)
188
188
  : authoredRich(el) ? window.ManifestRichText.sanitize(html, true, true) : leafClean(html, el);
189
+ // Text written into an element. A leaf shows exactly what is logged; a rich field owns its markup:
190
+ // it is written only when it differs as logged, then settles (keeping its editing affordances)
191
+ function writeText(el, html) {
192
+ const safe = sanitizeFor(el, html);
193
+ if (el.innerHTML === safe || (authoredRich(el) && sanitizeFor(el, el.innerHTML.trim()) === safe)) return;
194
+ el.innerHTML = safe;
195
+ if (el._rt && typeof el._rt.settle === 'function') el._rt.settle();
196
+ }
189
197
 
190
198
  function sanitizeHTML(html) {
191
199
  const t = document.createElement('template'); t.innerHTML = String(html == null ? '' : html);
@@ -529,7 +537,7 @@
529
537
  const base = area._baseText || {}, baseC = area._baseClass || {}, baseS = area._baseStyle || {};
530
538
  const pick = (prop, fallback) => iv[prop] !== undefined ? iv[prop] : (mv[prop] !== undefined ? mv[prop] : fallback);
531
539
  const text = pick('text', base[p]), cls = pick('class', baseC[p]), sty = pick('style', baseS[p]);
532
- if (text !== undefined) { const safe = sanitizeFor(el, text); if (el.innerHTML !== safe) el.innerHTML = safe; }
540
+ if (text !== undefined) writeText(el, text);
533
541
  if (cls !== undefined) writeClass(el, cls);
534
542
  if (sty !== undefined && el.getAttribute('style') !== sty) { if (sty === '') el.removeAttribute('style'); else el.setAttribute('style', sty); }
535
543
  });
@@ -540,7 +548,7 @@
540
548
  if (path == null) return false;
541
549
  if (prop === 'text') value = sanitizeFor(el, value);
542
550
  if (before === undefined) before = prop === 'text' ? el._preEdit : prop === 'style' ? el._preStyle : el._preClass;
543
- if (before === value) return false;
551
+ if (before === value || (prop === 'text' && before != null && sanitizeFor(el, before) === value)) return false;
544
552
  const sig = nodeSig(el);
545
553
  pushDelta(scope === 'main' ? { kind: 'cmp-main', component, path, prop, value, before, sig, region } : { kind: 'cmp-inst', component, region, path, prop, value, before, sig });
546
554
  applyComponentState();
@@ -640,7 +648,7 @@
640
648
 
641
649
  function applyStaticProp(el, prop, v) {
642
650
  if (v === undefined) return;
643
- if (prop === 'text') { const safe = sanitizeFor(el, v); if (el.innerHTML !== safe) el.innerHTML = safe; return; }
651
+ if (prop === 'text') return writeText(el, v);
644
652
  if (attrRefusal(prop, v, el.tagName)) return;
645
653
  if (prop === 'class') return writeClass(el, v);
646
654
  if (prop === 'style') return writeStyle(el, v);
@@ -862,8 +870,9 @@
862
870
  const pre = prop === 'text' ? el._preEdit : prop === 'class' ? el._preClass : prop === 'style' ? el._preStyle : undefined;
863
871
  before = pre !== undefined ? pre : fallback;
864
872
  }
865
- if (before === value) return false;
866
- if (prop === 'text' && document.activeElement !== el && el.innerHTML.trim() !== value) el.innerHTML = value;
873
+ if (before === value || (prop === 'text' && before != null && sanitizeFor(el, before) === value)) return false;
874
+ // A rich field owns its markup: the logged value is never written back into it
875
+ if (prop === 'text' && document.activeElement !== el && !authoredRich(el)) writeText(el, value);
867
876
  const last = log[cursor - 1];
868
877
  if (opts.coalesce && last && cursor === log.length && last.kind === 'st-node' && last.region === region && last.path === path && last.prop === prop && Date.now() - (stamps.get(last) || 0) < COALESCE_MS) {
869
878
  if (last.before === value) { log.pop(); cursor--; } else { last.value = value; stamps.set(last, Date.now()); }
@@ -901,7 +910,7 @@
901
910
  value = String(value);
902
911
  if (el && el._editRich) value = sanitizeFor(el, value);
903
912
  const before = el ? el._preEdit : undefined;
904
- if (before === value) return false;
913
+ if (before === value || (el && el._editRich && before != null && sanitizeFor(el, before) === value)) return false;
905
914
  pushDelta({ kind: 'data-val', region: key(area), source, id, field, value, before });
906
915
  applyDataValues();
907
916
  return true;
@@ -1163,7 +1172,8 @@
1163
1172
  if (kind === 'component') return false;
1164
1173
  const block = asPiece(el);
1165
1174
  if (!block) return false;
1166
- if (action === 'copy' || action === 'cut') return !isHostEl(block);
1175
+ if (action === 'copy') return !isHostEl(block);
1176
+ if (action === 'cut') return canDo('copy', block) && canDo('remove', block); // a cut is a copy and a remove
1167
1177
  if (action === 'move') return movable(block);
1168
1178
  const lockAs = action === 'duplicate' ? 'duplicate' : 'remove';
1169
1179
  return capOf(block.parentElement, 'move') && !lockedFor(block, lockAs);
@@ -1408,6 +1418,8 @@
1408
1418
  const region = s.crossOK ? dropArea(x, y, s.area) : s.area;
1409
1419
  if (!region) return null;
1410
1420
  const hit = document.elementFromPoint ? document.elementFromPoint(x, y) : null;
1421
+ // Over the stand-in itself: it stays where it is
1422
+ if (hit && s.ghost && (hit === s.ghost || s.ghost.contains(hit))) return null;
1411
1423
  if (hit && hit !== region && region.contains(hit)) {
1412
1424
  // Inside a component host: the host is what is hit (its markup is another file's)
1413
1425
  let from = hit;
@@ -1417,6 +1429,9 @@
1417
1429
  const parent = el.parentElement;
1418
1430
  if (!isContainer(parent)) continue;
1419
1431
  if (isHostEl(el)) { if (accepts(parent) && fits(s.item, parent) && !s.item.contains(parent)) return beside(el, x, y, parent); continue; }
1432
+ // Over the container the piece came from, or one holding the stand-in (a gap, its padding):
1433
+ // it stays in, placed by reading order; it leaves only when the pointer does
1434
+ if (s.ghost && (el === s.home || el.contains(s.ghost)) && accepts(el) && fits(s.item, el)) return { container: el, ref: afterElement(el, x, y) };
1420
1435
  if (isContainer(el) && accepts(el) && fits(s.item, el) && inMiddle(el, x, y, parent)) {
1421
1436
  if (s.ghost && s.ghost.parentElement !== el) s.nested = { el, rect: el.getBoundingClientRect(), horiz: flowAxis(parent) === 'x' };
1422
1437
  return { container: el, ref: afterElement(el, x, y) };
@@ -1429,9 +1444,12 @@
1429
1444
  }
1430
1445
  // Reading-order insertion: the first sibling past the pointer.
1431
1446
  function afterElement(c, x, y) {
1447
+ const column = flowAxis(c) === 'y';
1432
1448
  for (const el of sortableChildren(c)) {
1433
1449
  if (el === dragged || el.hasAttribute('data-edit-dragging') || lockedFor(el, 'move')) continue;
1434
1450
  const b = el.getBoundingClientRect(), cx = b.left + b.width / 2, cy = b.top + b.height / 2;
1451
+ // Stacked rows: past a row once the pointer is past its middle (wherever it is across)
1452
+ if (column && !/inline/.test(getComputedStyle(el).display)) { if (cy > y) return el; continue; }
1435
1453
  const sameRow = Math.abs(cy - y) < b.height / 2;
1436
1454
  if (cy - y > b.height / 2 || (sameRow && cx > x)) return el;
1437
1455
  }
@@ -2041,7 +2059,7 @@
2041
2059
  area.querySelectorAll('[data-rich-text]:not([data-rich-text-bound])').forEach(el => {
2042
2060
  if (el._editRt || !capOf(el, 'text') || !/^[@~]/.test(el.getAttribute('data-edit-key') || '')) return;
2043
2061
  if (el._richBound) return; el._richBound = true;
2044
- el.addEventListener('focusin', () => { el._preEdit = el.innerHTML.trim(); });
2062
+ el.addEventListener('focusin', () => { el._preEdit = leafText(el); });
2045
2063
  el.addEventListener('blur', () => {
2046
2064
  const a = regionOf(el) || area; // the region the element sits in NOW, not where it was armed
2047
2065
  const commit = classify(a) === 'component' ? commitComponentNode : commitStaticNode;
@@ -2298,12 +2316,25 @@
2298
2316
  el._cssvar.persist = !!(modifiers && modifiers.includes('persist'));
2299
2317
  cssvarControls.add(el);
2300
2318
  }
2319
+ // A colour input takes #rrggbb only: any CSS colour (a theme's oklch(), var() resolved) is read off a canvas pixel
2320
+ let colorCanvas = null;
2321
+ function hexColor(v) {
2322
+ if (/^#[0-9a-f]{6}$/i.test(v)) return v.toLowerCase();
2323
+ try {
2324
+ colorCanvas = colorCanvas || document.createElement('canvas');
2325
+ const g = colorCanvas.getContext && colorCanvas.getContext('2d', { willReadFrequently: true });
2326
+ if (!g) return null;
2327
+ const probe = document.createElement('i'); probe.style.color = v; if (!probe.style.color) return null;
2328
+ g.clearRect(0, 0, 1, 1); g.fillStyle = '#000'; g.fillStyle = v; g.fillRect(0, 0, 1, 1);
2329
+ return '#' + [...g.getImageData(0, 0, 1, 1).data.slice(0, 3)].map(x => x.toString(16).padStart(2, '0')).join('');
2330
+ } catch { return null; }
2331
+ }
2301
2332
  function syncCssVarInput(el) {
2302
2333
  const cv = el._cssvar; if (!cv || !('value' in el)) return;
2303
2334
  const cur = getComputedStyle(themeTargetEl(cv.scope)).getPropertyValue(cv.var).trim();
2304
2335
  if (!cur) return;
2305
- const next = el.dataset.unit ? String(parseFloat(cur)) : cur;
2306
- if (el.value !== next) el.value = next;
2336
+ const next = el.type === 'color' ? hexColor(cur) : el.dataset.unit ? String(parseFloat(cur)) : cur;
2337
+ if (next != null && el.value !== next) el.value = next;
2307
2338
  }
2308
2339
  function armCssVar(el) {
2309
2340
  const cv = el._cssvar; if (!cv || !cv.var) return;
@@ -2332,10 +2363,24 @@
2332
2363
  });
2333
2364
  cssvarControls.forEach(syncCssVarInput);
2334
2365
  }
2335
- function rebaseTheme() {
2336
- Object.keys(themeBaseline).forEach(kk => { const sep = kk.indexOf('|'); themeBaseline[kk] = themeTargetEl(kk.slice(0, sep) || null).style.getPropertyValue(kk.slice(sep + 1)); });
2337
- themeTouched.clear();
2338
- }
2366
+ function rebaseTheme(keep) {
2367
+ Object.keys(themeBaseline).forEach(kk => { if (keep && keep.has('theme|' + kk)) return; const sep = kk.indexOf('|'); themeBaseline[kk] = themeTargetEl(kk.slice(0, sep) || null).style.getPropertyValue(kk.slice(sep + 1)); });
2368
+ [...themeTouched].forEach(kk => { if (!(keep && keep.has('theme|' + kk))) themeTouched.delete(kk); });
2369
+ }
2370
+ // A scoped value lives in its scope element's inline style, and is written there, never to a
2371
+ // stylesheet: with its x-edit area's tree when the element is a static piece of one ('area'),
2372
+ // else found by its own x-edit.theme name ('own'). Inside generated content it can't be (null).
2373
+ function inlineScope(scope) {
2374
+ const el = scope && themeScopes[scope]; if (!el || generatedBy(el)) return null;
2375
+ const a = regionOf(el), k = el.getAttribute('data-edit-key');
2376
+ if (a && classify(a) === 'static' && !sharedKey(a) && k && /^[@~]/.test(k)) return 'area';
2377
+ if (a && a !== el) return null;
2378
+ const named = areaByKey(scope);
2379
+ return !named || named === el ? 'own' : null;
2380
+ }
2381
+ const inlineScoped = () => new Set(Object.values(themeState()).filter(d => d.scope && inlineScope(d.scope) === 'area').map(d => themeScopes[d.scope]));
2382
+ // The component file a scope element is authored in, if any.
2383
+ const scopeComponent = (el) => { for (let n = el; n; n = n.parentElement) if (n._manifestComponent) return n._manifestComponent; return null; };
2339
2384
  // A custom-property value must stay inside its declaration (same tokenizer as the
2340
2385
  // source writer): strings and url(...) are opaque; script schemes in url() and
2341
2386
  // expression() are refused anywhere; data: urls only as images.
@@ -2383,12 +2428,16 @@
2383
2428
  const cur = themeState()[kk]; if (cur && cur.value === value) return; // dedupe
2384
2429
  pushDelta({ kind: 'theme', scope: scope || null, var: v, value }, !!persist);
2385
2430
  }
2386
- // Each var → its scope's data-edit-theme-file, else the server's theme file.
2431
+ // Global vars → the page's theme stylesheet (the server's choice); scoped ones inline
2387
2432
  function themePatches() {
2388
- return Object.values(themeState()).map(d => {
2389
- const el = d.scope ? themeScopes[d.scope] : null;
2390
- return { kind: 'theme', scope: d.scope || null, var: d.var, value: d.value, file: el ? el.getAttribute('data-edit-theme-file') || null : null };
2433
+ const st = Object.values(themeState()), out = [];
2434
+ st.filter(d => !inlineScope(d.scope)).forEach(d => out.push({ kind: 'theme', scope: d.scope || null, var: d.var, value: d.value, file: null }));
2435
+ // A scope element of its own: its style attribute, as the page has it now
2436
+ new Set(st.filter(d => d.scope && inlineScope(d.scope) === 'own').map(d => d.scope)).forEach(scope => {
2437
+ const el = themeScopes[scope], comp = scopeComponent(el), tree = { key: '', edits: { style: ownStyle(el) || null } };
2438
+ out.push(comp ? { kind: 'static-tree', region: scope, component: comp, tree } : { kind: 'static-tree', region: scope, tree });
2391
2439
  });
2440
+ return out;
2392
2441
  }
2393
2442
 
2394
2443
 
@@ -2448,8 +2497,21 @@
2448
2497
  };
2449
2498
  walk(area, '');
2450
2499
  }
2451
- const warnedRendered = new Set(), renderedRegions = new Set();
2500
+ const warnedRendered = new Set(), renderedRegions = new Set(), unwritableWhy = new Map();
2501
+ // A list whose records mnfst-run can't write: inline x-data, or a source manifest.json does not declare as a file.
2502
+ function dataUnwritable(area) {
2503
+ if (classify(area) !== 'data') return null;
2504
+ const m = (dataSourceExpr(area) || '').match(/\$x\.([\w$]+)/);
2505
+ if (!m) return 'inline x-data list: mnfst-run writes data only to CSV/JSON/YAML sources declared in manifest.json';
2506
+ const mf = window.ManifestComponentsRegistry && window.ManifestComponentsRegistry.manifest;
2507
+ if (!mf) return null;
2508
+ const src = (mf.data || {})[m[1]];
2509
+ if (src == null) return `data source '${m[1]}' is not declared in manifest.json: mnfst-run writes data only to CSV/JSON/YAML files declared there`;
2510
+ return typeof src === 'string' ? null : `data source '${m[1]}' is not a file (a cloud or remote source): save it with $x.${m[1]}.$update, not mnfst-run`;
2511
+ }
2452
2512
  function armArea(area) {
2513
+ const dw = dataUnwritable(area);
2514
+ if (dw) { renderedRegions.add(key(area)); unwritableWhy.set(key(area), dw); if (!warnedRendered.has(key(area))) { warnedRendered.add(key(area)); console.warn(`[edit] x-edit="${key(area)}": ${dw}; its edits stay on the page (and reach onPublish)`); } }
2453
2515
  if (area._edit.rendered) renderedRegions.add(key(area));
2454
2516
  if (area._edit.rendered && !warnedRendered.has(key(area))) { warnedRendered.add(key(area)); console.warn(`[edit] x-edit="${key(area)}" is inside content ${area._edit.rendered} renders: it edits on the page (and reaches onPublish), but mnfst-run cannot write it to a source file`); }
2455
2517
  uneditIn(null);
@@ -2545,7 +2607,7 @@
2545
2607
  return Object.keys(e).length ? e : null;
2546
2608
  };
2547
2609
  const nontrivial = (n) => n.edits || n.children || n.key[0] === '~';
2548
- const live = keyMap(staticAreas());
2610
+ const live = keyMap(staticAreas()), scoped = inlineScoped();
2549
2611
  staticAreas().forEach(area => {
2550
2612
  const region = key(area), comp = sourceComponent(area);
2551
2613
  const ids = new Proxy({}, { get: (_, k) => { const o = originOf(String(k)), a = areaByKey(decodeURIComponent(o.slice(1, o.indexOf('@', 1)))); const m = baseIds && a && baseIds[sourceFile(a)]; return m ? m[o] : undefined; } });
@@ -2565,6 +2627,7 @@
2565
2627
  const at = originAt.get(originOf(id)); if (at != null) n.at = at;
2566
2628
  if (ids && ids[originOf(id)]) n.fp = ids[originOf(id)];
2567
2629
  const e = editsFor(id); if (e) n.edits = e;
2630
+ if (scoped.has(el)) n.edits = Object.assign({}, n.edits, { style: ownStyle(el) || null });
2568
2631
  if (!(e && 'text' in e) && !isRendered(el) && !el.hasAttribute('data-rich-text')) {
2569
2632
  const kids = treeKids(el).map(c => build(c)).filter(Boolean);
2570
2633
  const structural = ss.touched.has(isRoot ? '#' + region : id);
@@ -2606,7 +2669,8 @@
2606
2669
  return e;
2607
2670
  };
2608
2671
  const dv = dataValueState();
2609
- Object.entries(dv).forEach(([source, recs]) => Object.entries(recs).forEach(([id, fields]) => Object.entries(fields).forEach(([field, value]) => patches.push({ kind: 'data-val', source, id, field, value }))));
2672
+ const dvRegion = {}; log.slice(0, cursor).forEach(d => { if (d.kind === 'data-val') dvRegion[d.source] = d.region; });
2673
+ Object.entries(dv).forEach(([source, recs]) => Object.entries(recs).forEach(([id, fields]) => Object.entries(fields).forEach(([field, value]) => patches.push({ kind: 'data-val', region: dvRegion[source], source, id, field, value }))));
2610
2674
  const { main, inst, reverts } = componentState();
2611
2675
  Object.entries(main).forEach(([component, paths]) => { const edits = toEdits(paths); if (edits.length) patches.push({ kind: 'component', scope: 'main', component, edits }); });
2612
2676
  new Set([...Object.keys(inst), ...Object.keys(reverts)]).forEach(region => {
@@ -2623,7 +2687,7 @@
2623
2687
  // `keep` (areas a publish left out), whose identities are left as they are.
2624
2688
  function rebase(keep) {
2625
2689
  const mounted = (r) => r == null || (!!areaByKey(r) && !(keep && keep.has(r)));
2626
- const pending = (d) => (d.kind === 'st-move' ? [d.from, d.to] : [d.region]).some(r => !mounted(r));
2690
+ const pending = (d) => d.kind === 'theme' ? !!keep && (keep.has('theme|' + (d.scope || '') + '|' + d.var) || keep.has(d.scope)) : (d.kind === 'st-move' ? [d.from, d.to] : [d.region]).some(r => !mounted(r));
2627
2691
  // Runtime locks are app state, not source: carried over by element.
2628
2692
  const locks = [];
2629
2693
  const lockState = lockStateNow();
@@ -2657,7 +2721,7 @@
2657
2721
  if (kind === 'component' && a._armed) armComponent(a);
2658
2722
  lastSnap[key(a)] = snapshot(a, kind);
2659
2723
  });
2660
- rebaseTheme();
2724
+ rebaseTheme(keep);
2661
2725
  relock();
2662
2726
  saveState(); refresh();
2663
2727
  }
@@ -2783,7 +2847,7 @@
2783
2847
  // fingerprints of keys with twins there (which may not be the elements this page shows).
2784
2848
  let overNewer = false;
2785
2849
  function publishNow(opts) {
2786
- const done = (r, keep) => {
2850
+ const done = (r, keep, heldRes) => {
2787
2851
  if (r && r.base && baseFiles) Object.entries(r.base).forEach(([f, h]) => { if (!overNewer || !/\.html?$/i.test(f)) baseFiles[f] = h; });
2788
2852
  if (r && r.ids && baseIds) {
2789
2853
  if (!overNewer) baseIds = r.ids;
@@ -2792,7 +2856,7 @@
2792
2856
  const stale = r && Array.isArray(r.stale) ? r.stale : [];
2793
2857
  if (estore) estore.stale = stale;
2794
2858
  if (stale.length) document.dispatchEvent(new CustomEvent('edit:stale', { detail: { files: stale } }));
2795
- if (published(keep ? { results: r.results.filter(x => !(x && keep.has(x.region))) } : r)) rebase(keep);
2859
+ if (published(keep ? { results: r.results.filter(x => !(x && (keep.has(x.region) || (heldRes && heldRes.has(x))))) } : r)) rebase(keep);
2796
2860
  takeHeldReload();
2797
2861
  return r;
2798
2862
  };
@@ -2801,14 +2865,19 @@
2801
2865
  const leftOut = (r, patches) => {
2802
2866
  if (!r || !Array.isArray(r.results) || (Array.isArray(r.stale) && r.stale.length)) return null;
2803
2867
  const failed = new Set(r.results.filter(x => x && x.failed != null).map(x => x.failed));
2804
- if (!failed.size || r.results.some((x, i) => x && x.status === 'error' && !failed.has(patches[i] && patches[i].region))) return null;
2868
+ const own = new Set(patches.filter((p, i) => r.results[i] && r.results[i].status === 'error' && r.results[i].failed == null && p.kind !== 'static-tree'));
2869
+ if ((!failed.size && !own.size) || r.results.some((x, i) => x && x.status === 'error' && !failed.has(patches[i] && patches[i].region) && !own.has(patches[i]))) return null;
2805
2870
  tie(failed);
2806
- return patches.some(p => !failed.has(p.region)) ? failed : null;
2871
+ const out = (p) => own.has(p) || (p.region != null && failed.has(p.region));
2872
+ if (!patches.some(p => !out(p))) return null;
2873
+ return Object.assign(failed, { own, out });
2807
2874
  };
2875
+ // What a held-back patch keeps in the log: its region's edits, or its theme value.
2876
+ const keepOf = (p) => p.kind === 'theme' ? 'theme|' + (p.scope || '') + '|' + p.var : p.region;
2808
2877
  // An area inside generated content (x-markdown output and the like) edits on the page,
2809
2878
  // but its markup is in no file mnfst-run can write: it is refused by name, not sent.
2810
2879
  const renderedBy = (region) => { const a = areaByKey(region); return a && a._edit.rendered; };
2811
- const unwritable = (p) => (p.kind === 'static-tree' || (p.kind === 'component' && p.scope === 'instance')) && !!renderedBy(p.region);
2880
+ const unwritable = (p) => p.region != null && p.kind !== 'theme' && renderedRegions.has(p.region);
2812
2881
  const send = () => {
2813
2882
  commitFocused(true); // typing under way publishes with the rest (identities are re-derived from the page after)
2814
2883
  const patches = buildPatches();
@@ -2816,24 +2885,25 @@
2816
2885
  if (!patches.length) { takeHeldReload(); return Promise.resolve({ results: [] }); } // nothing to send: a held reload goes now
2817
2886
  const post = (ps) => { inFlight++; return fetch('/__edit/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ patches: ps, base: baseFiles }) }).then(r => r.json()).catch(err => { settled(null); throw err; }); };
2818
2887
  const off = tie(new Set(patches.filter(unwritable).map(p => p.region)));
2819
- const offResult = (p) => ({ region: p.region, status: 'error', reason: renderedBy(p.region) && unwritable(p)
2820
- ? `x-edit="${p.region}" is inside content ${renderedBy(p.region)} renders, so mnfst-run can't write it to a source file`
2888
+ const offResult = (p) => ({ region: p.region, status: 'error', reason: unwritableWhy.has(p.region) && unwritable(p) ? `x-edit="${p.region}": ${unwritableWhy.get(p.region)}`
2889
+ : renderedBy(p.region) && unwritable(p) ? `x-edit="${p.region}" is inside content ${renderedBy(p.region)} renders, so mnfst-run can't write it to a source file`
2821
2890
  : `not published: tied by a move to x-edit="${off.via.get(p.region)}", which mnfst-run can't write` });
2822
2891
  const sendable = patches.filter(p => !off.has(p.region));
2823
2892
  // The sent part's answer, merged with what was held back, as one result per patch.
2824
2893
  const finish = (r, held, sent) => {
2825
- const keep = new Set([...off, ...(held || [])]);
2826
- const results = patches.map(p => off.has(p.region) ? offResult(p) : r.results[sent.indexOf(p)]);
2827
- const o = done(Object.assign({}, r, { results }), keep.size ? keep : undefined); settled(r, !!(opts && opts.rebase)); return o;
2894
+ const keep = new Set([...off, ...(held || []), ...(held ? sent.filter(held.out).map(keepOf) : [])]);
2895
+ const heldRes = new Set();
2896
+ const results = patches.map(p => { if (off.has(p.region)) { const x = offResult(p); heldRes.add(x); return x; } const x = r.results[sent.indexOf(p)]; if (held && held.out(p)) heldRes.add(x); return x; });
2897
+ const o = done(Object.assign({}, r, { results }), keep.size ? keep : undefined, heldRes); settled(r, !!(opts && opts.rebase)); return o;
2828
2898
  };
2829
2899
  if (!sendable.length) { const o = done({ results: patches.map(offResult) }, off); return Promise.resolve(o); }
2830
2900
  return post(sendable).then(r => {
2831
2901
  const out = leftOut(r, sendable);
2832
2902
  if (!out) return finish(r, null, sendable);
2833
2903
  settled(r, false);
2834
- const rest = sendable.filter(p => !out.has(p.region));
2904
+ const rest = sendable.filter(p => !out.out(p));
2835
2905
  return post(rest).then(r2 => {
2836
- const results = sendable.map((p, i) => out.has(p.region)
2906
+ const results = sendable.map((p, i) => out.out(p)
2837
2907
  ? (r.results[i] && r.results[i].status === 'error' ? r.results[i] : { region: p.region, status: 'error', reason: `not published: tied by a move to x-edit="${out.via.get(p.region) || [...out].find(x => !out.via.has(x))}", which failed` })
2838
2908
  : r2.results[rest.indexOf(p)]);
2839
2909
  return finish(Object.assign({}, r2, { results }), out, sendable);
@@ -2874,7 +2944,8 @@
2874
2944
  ancestor opts it out. Text that is still being typed keeps the browser's own
2875
2945
  undo until it commits on blur; fields and x-rich-text keep theirs always. A leaf
2876
2946
  on Rich Text uses Rich Text's history while it has steps (richHistoryKey). */
2877
- const leafText = (el) => el._editField && !el._editRich ? el.textContent : el._editRt ? sanitizeFor(el, el.innerHTML.trim()) : el.innerHTML.trim();
2947
+ // Text as it would be logged: every baseline and comparison goes through the same sanitizer
2948
+ const leafText = (el) => el._editField && !el._editRich ? el.textContent : sanitizeFor(el, el.innerHTML.trim());
2878
2949
  const hotkeysOff = (el) => !!el.closest('[data-edit-hotkeys="off"]');
2879
2950
  // A focused leaf's baseline follows history rewrites under the caret.
2880
2951
  function rebaseFocused() {
@@ -2935,8 +3006,10 @@
2935
3006
  refresh();
2936
3007
  }
2937
3008
  function waitForData(area, cb) { const expr = dataSourceExpr(area), tpl = listTpl(area); let n = 0; const t = setInterval(() => { let r = false; try { r = Array.isArray(window.Alpine.evaluate(tpl, expr)); } catch { } if (r) { clearInterval(t); cb(); } else if (++n > 100) clearInterval(t); }, 50); }
3009
+ let inited = false;
2938
3010
  function init() {
2939
- if (!window.Alpine || !Alpine.directive) return;
3011
+ if (inited || !window.Alpine || !Alpine.directive) return;
3012
+ inited = true;
2940
3013
  // Alpine walks only x-data trees: a page with none still gets its areas
2941
3014
  if (document.body && !document.body.hasAttribute('x-data')) document.body.setAttribute('x-data', '{}');
2942
3015
  Alpine.directive('edit', (el, { modifiers, expression }, { cleanup }) => {
@@ -2968,14 +3041,20 @@
2968
3041
  block(node) { return pieceOf(node); },
2969
3042
  attr(name, value, el) { return setAttr(name, value, el); },
2970
3043
  classes: classesApi,
2971
- scope(value, el) { void this.version; return value === undefined ? scopeOf(el) : setScope(value, el); },
3044
+ scope(value, el) { void this.version; if (value && value.nodeType === 1) return scopeOf(value); return value == null ? scopeOf(el) : setScope(value, el); },
2972
3045
  revert(el) { return revert(el); },
2973
3046
  clearConflicts() { conflicts = []; this.conflicts = []; saveState(); }
2974
3047
  });
2975
3048
  estore = Alpine.store('edit');
2976
- Alpine.magic('edit', () => Alpine.store('edit'));
3049
+ const editMagic = () => Alpine.store('edit');
3050
+ Alpine.magic('edit', editMagic);
3051
+ // The loader's stand-in answered $edit until now: it hands over, and effects that read it run again
3052
+ window.__manifestEditMagic = editMagic;
3053
+ const kept = window.__manifestEditStubWrites; // writes made on the stand-in ($edit.onPublish = …)
3054
+ if (kept) Object.keys(kept).forEach(k => { try { estore[k] = kept[k]; } catch { } delete kept[k]; });
3055
+ if (window.__manifestEditStub) setTimeout(() => { window.__manifestEditStub.v++; }, 0);
2977
3056
  loadState();
2978
- loadBase();
3057
+ if (window.__mnfstEdit) loadBase(); // only mnfst-run --edit serves the base
2979
3058
  setTimeout(() => { armAll(); restore(); }, 450);
2980
3059
  document.addEventListener('keydown', onHistoryKey);
2981
3060
  window.addEventListener('click', onAreaLinkClick, true);
@@ -2984,6 +3063,16 @@
2984
3063
  document.addEventListener('dropdown:context', onContextCapture, true);
2985
3064
  // SPA navigation: the state so far is saved under the page it was made on.
2986
3065
  window.addEventListener('manifest:route-change', () => { if (lsPath !== location.pathname) { lsPath = location.pathname; saveState(); } });
3066
+ adopt();
3067
+ }
3068
+ // Loaded after Alpine started: x-edit elements its walk passed get their attributes re-set
3069
+ // (Alpine runs them as new directives); those it never walked (no x-data above) are walked now.
3070
+ function adopt() {
3071
+ if (!window.__manifestAlpineStarted && ![...document.querySelectorAll('[x-data]')].some(e => e._x_marker)) return;
3072
+ const isEdit = (n) => /^x-edit(\.|$)/.test(n);
3073
+ const found = [...document.querySelectorAll('*')].filter(el => !el._edit && [...el.attributes].some(a => isEdit(a.name)));
3074
+ found.forEach(el => { if (el._x_marker) [...el.attributes].filter(a => isEdit(a.name)).forEach(a => el.setAttribute(a.name, a.value)); });
3075
+ if (found.some(el => !el._x_marker)) Alpine.initTree(document.body);
2987
3076
  }
2988
3077
  document.addEventListener('alpine:init', init);
2989
3078
  if (window.Alpine && Alpine.directive) setTimeout(init, 0);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest.appwrite.auth.js": "sha384-dsIWnH0qCAGSzAXMGrd8fvOrVl4tOIE6j5tC35JPTrQXZ56qLD36ARyvhR7vi3Id",
3
- "manifest.appwrite.data.js": "sha384-enlQBS4B79OAlkgYX0qon8pnr3Twc7O1iGtiQ+L+Hqgoi+rCKA3FOUFrTBuhprxV",
3
+ "manifest.appwrite.data.js": "sha384-pj/2ZCUMXz3zTWPZyICJHfCPnz5srho9URrw0Sl0nfWuvh5KqvoT/g3Kg4uyv7yl",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
5
  "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
6
6
  "manifest.charts.js": "sha384-gEAv/98+29RbhvNkiNWTNyBPbLTFr2Ld15wLu29FLCL57j1cwGChRrL9Pfk7dBtT",
@@ -11,24 +11,24 @@
11
11
  "manifest.combobox.js": "sha384-MILTkrgsTzsYUjGo4ZuADqV85Xs4GmfB4xexfe9vbJMIoQTZ42W4zs+DTaVSPEsx",
12
12
  "manifest.components.js": "sha384-JalISaBBWNFl3ggx92Hb3B52EpwwG3gzgra6Q+js0OgZ8vrFKMD48CuBox2N9A5T",
13
13
  "manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
14
- "manifest.data.js": "sha384-szQophuvtkatLyTtVALy63mbKPGQZQ/lP2v27QExQnS7HMuCbuhxgKdTITZQ1JQq",
14
+ "manifest.data.js": "sha384-+wRqEd8FFPqaSX5gZhb66OgkAHTiSQDFK6W1agFMceKtd5nVL7OLh6p5gC96AJ3Z",
15
15
  "manifest.datepicker.js": "sha384-XnbeURni2X91ozVzatQcSjYYIS4MoW12dBRJnYAGBr0nCQ624heH5s+xtQSpLZX6",
16
16
  "manifest.defer.js": "sha384-pMRGqQKiEoJhpUcD40QeM4jEHau7sz9Xn/YCpiUamT1T4xJBZDSiYX+xnVrSWoq+",
17
17
  "manifest.device.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
18
18
  "manifest.dropdowns.js": "sha384-acufUps7R9WjqB4XXW5JLACqniuHJGM4CNTb2rF4B2Q/jv7ZRmsAwfr2McanOz3s",
19
- "manifest.edit.js": "sha384-UsKV6oSwdF+yFtpZKr8KD62VajynYVsBcVN3+dAUI4nh2pOOf3/2lyybXbEG/pvT",
19
+ "manifest.edit.js": "sha384-XUtJ+EXXYh+5afgYuVpXurjvylSP/o72Y4mQR5wxdkIFQ/NDFBVvRFAfbPXMInZc",
20
20
  "manifest.export.js": "sha384-Rx6XvqBZc4apzP/FwcDr6yIln4/zNa08Q4ZqBZcEqUz1aNZQfl4h0+Ae7mqCQyuG",
21
21
  "manifest.icons.js": "sha384-kKOINpX70YzNO3FvkV6umrmO3f0KIu0DNbwB+ej55a7y/lhU7v/myQtXL0bwCTbt",
22
22
  "manifest.localization.js": "sha384-EImsIw8Z3l13V5eH9ZJF/yfsOfIvR1oPRNSArANkPji7V8O3IRf6zMwsNiAGj3Hk",
23
23
  "manifest.markdown.js": "sha384-heyDR40LXHaAmAuA+fa381uFwShk1oAqpFFY6E2Qq4T3VN0TQfRqyc++MF40K5Hj",
24
24
  "manifest.native.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
25
25
  "manifest.payments.js": "sha384-8pofk2zhxtapPMpwnFyTelPMrYhShNhDCE722uQh/EYFvZFuPLmSdgunqsarVQjv",
26
- "manifest.rich.text.js": "sha384-CBScTgPU6y96e481M7Ba1/YqPBkeQNLvMKpGc9LQLALOdyj8wlv7Faez+MXjT9yn",
26
+ "manifest.rich.text.js": "sha384-yJNb+EIumFIJx2KiKmLojwyHLCKwIn+48noxh3gUuZ10YZOvJJoanzAU7NLIBF1s",
27
27
  "manifest.router.js": "sha384-QkKhRaCbdXEOxhj+ojT4jeLUCtQepBnqP+QWTIWdb6kUWD+mVzPN8hf6jhgschpW",
28
28
  "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
29
29
  "manifest.status.js": "sha384-MBO345ab7H6Uq1X2dZLt4Ge1Gcra+Z2qYApvlAeIV8CJhDDJrz2FPXpp+Kcp8zcM",
30
30
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
31
- "manifest.sw.js": "sha384-FiEZfwCVI2lKcp4xaRE6nJhjGIbphg6BREeTGvRsIMpqqJ+5AzxFQKE1o84fGfGE",
31
+ "manifest.sw.js": "sha384-alPjmWaugQV/aCb6l7hVITwZ6Z5k47SJ9jHXmNUN093iUu8ikbVggwLSGNb84Ib1",
32
32
  "manifest.tabs.js": "sha384-OIIcyaQnGS0eNYk4zgAFEDboNYect7THvUd9D0V6nqCeDOxQcoiqLrBEmmWVTTet",
33
33
  "manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
34
34
  "manifest.toasts.js": "sha384-3zkWxzw693pxg3QDFsoYCA3CqYaLKmBaMArULXODD66Utolpewl0OAeTB92aWUVv",
@@ -36,5 +36,5 @@
36
36
  "manifest.url.parameters.js": "sha384-J2aSVBOPsoqsbafHndrWLHEDpKb4AKJAoQnVmKA4gZh5P/8HHjFWHO/lkusmG84l",
37
37
  "manifest.utilities.js": "sha384-L5uXMLFBIF6LUM1haRjd4eGjHSWuqsCYSpPZRPYosNfJsMC5U5TnmjBxIhrZRe2d",
38
38
  "manifest.virtual.js": "sha384-ZDNoF9QVhVcVpkjIK6NH15S6I5c6OBshiJpXSOYjNsIB+298I0a7g3aFO+jyq4Vb",
39
- "manifest.js": "sha384-G+ctciC90o/zSB/sRbmX7ANE/7rYvnepyzIsvNbD6N5ynlO5oludSDfzYVCdzU/g"
39
+ "manifest.js": "sha384-m5WiqHv+23ZnBvvIt9NtUGE2Gupaw9jDpKuonIH9HZCHc+LR8wbitDImJoNPH9CI"
40
40
  }
package/lib/manifest.js CHANGED
@@ -597,14 +597,55 @@
597
597
  const A = window.Alpine;
598
598
  if (window.ManifestRichText || !A || !A.magic || !A.reactive || window.__manifestRichStub) return;
599
599
  const state = window.__manifestRichStub = A.reactive({ v: 0 });
600
- const idle = new Proxy({ value: '', link: '', selection: null, all: {}, active: () => false, can: () => false, markdown: () => '', html: () => '' },
601
- { get: (t, k) => k in t ? t[k] : (typeof k === 'string' ? () => { } : undefined), set: () => true });
602
- A.magic('rich', (el, u) => { void state.v; return window.__manifestRichMagic ? window.__manifestRichMagic(el, u) : idle; });
600
+ // Writes ($rich.value = …, $rich.all = …) are kept per element and applied to its field at handover
601
+ const writes = window.__manifestRichStubWrites = [], perEl = new WeakMap();
602
+ const deref = (r) => r && typeof r.deref === 'function' ? r.deref() : r;
603
+ const base = { value: '', link: '', selection: null, all: {}, active: () => false, can: () => false, markdown: () => '', html: () => '' };
604
+ const idleFor = (el) => {
605
+ if (!perEl.has(el)) perEl.set(el, new Proxy(base, {
606
+ get: (t, k) => { const w = writes.find(x => deref(x[0]) === el && x[1] === k); return w ? w[2] : k in t ? t[k] : (typeof k === 'string' ? () => { } : undefined); },
607
+ // The latest write per element and key; elements gone from the page are let go
608
+ set: (t, k, v) => {
609
+ for (let i = writes.length - 1; i >= 0; i--) { const e = deref(writes[i][0]); if (!e || !e.isConnected || (e === el && writes[i][1] === k)) writes.splice(i, 1); }
610
+ writes.push([typeof WeakRef === 'function' ? new WeakRef(el) : el, k, v]);
611
+ return true;
612
+ }
613
+ }));
614
+ return perEl.get(el);
615
+ };
616
+ A.magic('rich', (el, u) => { void state.v; return window.__manifestRichMagic ? window.__manifestRichMagic(el, u) : idleFor(el); });
603
617
  };
604
618
  document.addEventListener('alpine:init', install, { once: true });
605
619
  if (window.Alpine && window.Alpine.magic) install();
606
620
  }
607
621
  stubRichMagic();
622
+ // $edit likewise: Edit loads behind a gate (data-plugins or Manifest.loadPlugin('edit')), so until
623
+ // it registers, $edit is an idle stand-in; Edit bumps `v` and the stand-in delegates to it.
624
+ let editStubArmed = false;
625
+ function stubEditMagic() {
626
+ if (typeof window === 'undefined' || typeof document === 'undefined' || editStubArmed) return;
627
+ editStubArmed = true;
628
+ const install = () => {
629
+ const A = window.Alpine;
630
+ if (window.__manifestEditMagic || !A || !A.magic || !A.reactive || window.__manifestEditStub) return;
631
+ const state = window.__manifestEditStub = A.reactive({ v: 0 });
632
+ const none = () => false;
633
+ // Writes ($edit.onPublish = …) are kept and applied to the store at handover
634
+ const writes = window.__manifestEditStubWrites = {};
635
+ const idle = new Proxy({
636
+ active: false, canUndo: false, canRedo: false, version: 0, stale: [], conflicts: [], onPublish: null, target: null,
637
+ can: none, scope: () => null, block: () => null, patches: () => [], export: () => ({ log: [], cursor: 0 }),
638
+ publish: () => Promise.resolve({ results: [] }), undo: () => Promise.resolve(false), redo: () => Promise.resolve(false),
639
+ classes: new Proxy({}, { get: () => none })
640
+ }, { get: (t, k) => k in writes ? writes[k] : k in t ? t[k] : (typeof k === 'string' ? none : undefined), set: (t, k, v) => { writes[k] = v; return true; } });
641
+ A.magic('edit', (el, u) => { void state.v; return window.__manifestEditMagic ? window.__manifestEditMagic(el, u) : idle; });
642
+ };
643
+ document.addEventListener('alpine:init', install, { once: true });
644
+ if (window.Alpine && window.Alpine.magic) install();
645
+ }
646
+ stubEditMagic();
647
+ // Plugins loaded after Alpine started adopt what its first walk already passed
648
+ if (typeof document !== 'undefined') document.addEventListener('alpine:initialized', () => { window.__manifestAlpineStarted = true; }, { once: true });
608
649
  // Framework web components — <x-*> tags that are NOT project components.
609
650
  const FRAMEWORK_TAG_RE = /^x-(code|code-group)$/;
610
651
  function detectUsagePlugins(alreadyLoaded, root = typeof document === 'undefined' ? null : document) {