mnfst 0.5.219 → 0.5.221

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.219';
5
+ const MANIFEST_BUILD_VERSION = '0.5.221';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
@@ -635,6 +635,12 @@ async function setupCodeGroup(group) {
635
635
  pre.appendChild(copyBtn);
636
636
  }
637
637
 
638
+ // Stamped only once the group is FULLY built (header, panels, copy). The
639
+ // re-bind pass keys off this, never off data-group-processed, which is
640
+ // claimed synchronously at entry — a scan landing between that claim and
641
+ // the awaited builds above would otherwise wire (and _groupBound-poison)
642
+ // a half-built group.
643
+ pre.dataset.groupBuilt = 'yes';
638
644
  wireGroup(pre);
639
645
  }
640
646
 
@@ -817,7 +823,7 @@ function processOne(el) {
817
823
  // editors arrive dead. Re-attach behavior to that adopted markup. Cheap (no
818
824
  // highlighting), so it runs eagerly rather than through the IntersectionObserver.
819
825
  function rebindAdopted(root = document) {
820
- root.querySelectorAll('[x-code-group][data-group-processed="yes"]').forEach(g => {
826
+ root.querySelectorAll('[x-code-group][data-group-built="yes"]').forEach(g => {
821
827
  wireGroup(g);
822
828
  Array.from(g.children).forEach(c => { if (c.dataset && c.dataset.codeBlockBuilt === 'yes') rebindBlock(c); });
823
829
  });
package/lib/manifest.css CHANGED
@@ -2946,18 +2946,18 @@
2946
2946
  itself; the affordance only has to be there when you go looking for it.
2947
2947
  While an inner affordance is engaged (text caret, handle, row), the region
2948
2948
  outline stands down so only the element being edited is highlighted. */
2949
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *) {
2949
+ :where([data-edit-armed][data-edit-authoring]):where([data-edit-ui], [data-edit-ui] *) {
2950
2950
  outline: 1px dashed transparent;
2951
2951
  outline-offset: 6px;
2952
2952
  border-radius: var(--radius);
2953
2953
  transition: outline-color 0.12s ease;
2954
2954
  }
2955
2955
 
2956
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *):is(:hover, :focus-within):not(:has([contenteditable]:is(:hover, :focus), [data-edit-handle]:is(:hover, :focus), [data-edit-sortable]:is(:hover, :focus-within), [data-edit-movable]:is(:hover, :focus))) {
2956
+ :where([data-edit-armed][data-edit-authoring]):where([data-edit-ui], [data-edit-ui] *):is(:hover, :focus-within):not(:has([contenteditable]:is(:hover, :focus), [data-edit-handle]:is(:hover, :focus), [data-edit-sortable]:is(:hover, :focus-within), [data-edit-movable]:is(:hover, :focus))) {
2957
2957
  outline-color: var(--color-line);
2958
2958
  }
2959
2959
 
2960
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *)::before {
2960
+ :where([data-edit-armed][data-edit-authoring]):where([data-edit-ui], [data-edit-ui] *)::before {
2961
2961
  content: attr(data-edit-label);
2962
2962
  position: absolute;
2963
2963
  inset-block-start: calc(-0.6em - 6px);
@@ -2975,7 +2975,7 @@
2975
2975
  transition: opacity 0.12s ease;
2976
2976
  }
2977
2977
 
2978
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *):is(:hover, :focus-within):not(:has([contenteditable]:is(:hover, :focus), [data-edit-handle]:is(:hover, :focus), [data-edit-sortable]:is(:hover, :focus-within), [data-edit-movable]:is(:hover, :focus)))::before {
2978
+ :where([data-edit-armed][data-edit-authoring]):where([data-edit-ui], [data-edit-ui] *):is(:hover, :focus-within):not(:has([contenteditable]:is(:hover, :focus), [data-edit-handle]:is(:hover, :focus), [data-edit-sortable]:is(:hover, :focus-within), [data-edit-movable]:is(:hover, :focus)))::before {
2979
2979
  opacity: 1;
2980
2980
  }
2981
2981
 
@@ -2986,29 +2986,9 @@
2986
2986
  transition: opacity 0.12s ease, outline-color 0.12s ease;
2987
2987
  }
2988
2988
 
2989
- /* The grip: the plugin's own inline drag affordance, sitting inside the row
2990
- at its inline start. A dot grid, revealed on approach. --edit-grip sizes
2991
- it; `--edit-grip: 0` removes it (e.g. for grid rows where an extra flex
2992
- item would take a cell). No full-row outline — the grip and the cursor
2993
- say "draggable" without boxing the content. */
2994
- :where([data-edit-ui] [data-edit-sortable], [data-edit-ui][data-edit-sortable])::before {
2995
- content: '';
2996
- align-self: center;
2997
- flex: 0 0 auto;
2998
- width: var(--edit-grip, 0.6rem);
2999
- height: 1rem;
3000
- margin-inline-end: calc(var(--edit-grip, 0.6rem) / 2);
3001
- background-image: radial-gradient(circle, var(--color-content-subtle) 1px, transparent 1px);
3002
- background-size: 0.3rem 0.3rem;
3003
- background-position: center;
3004
- opacity: 0;
3005
- transition: opacity 0.12s ease;
3006
- pointer-events: none;
3007
- }
3008
-
3009
- :where([data-edit-ui] [data-edit-sortable], [data-edit-ui][data-edit-sortable]):is(:hover, :focus-visible)::before {
3010
- opacity: 0.9;
3011
- }
2989
+ /* No injected grip: the row itself is the handle (grab cursor), while text
2990
+ inside stays a text surface (text cursor, below). Rows that are pure text
2991
+ still reorder by keyboard. */
3012
2992
 
3013
2993
  :where([data-edit-sortable], [data-edit-movable]):focus-visible {
3014
2994
  outline: 2px solid var(--edit-accent, var(--color-brand-content));
@@ -3031,6 +3011,14 @@
3031
3011
  pointer-events: none;
3032
3012
  }
3033
3013
 
3014
+ /* Valid cross-region destinations during a drag: a dashed invitation, and
3015
+ enough height that an EMPTY region can be dropped onto at all. */
3016
+ :where([data-edit-drop-target]) {
3017
+ outline: 1px dashed color-mix(in oklch, var(--edit-accent, var(--color-brand-content)) 55%, transparent);
3018
+ outline-offset: 4px;
3019
+ min-height: 2.5rem;
3020
+ }
3021
+
3034
3022
  /* Nothing else should react while a drag is in progress */
3035
3023
  :where([data-edit-dragging-in]) {
3036
3024
  cursor: grabbing;
@@ -3062,6 +3050,7 @@
3062
3050
  1px line, so focusing doesn't make the page jump a weight class. */
3063
3051
  :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"],
3064
3052
  :where([data-edit-ui])[data-edit-armed] [contenteditable="true"] {
3053
+ cursor: text;
3065
3054
  outline: 1px solid transparent;
3066
3055
  outline-offset: 2px;
3067
3056
  border-radius: 3px;
@@ -4261,13 +4250,9 @@
4261
4250
  @layer components {
4262
4251
 
4263
4252
  :where([data-text-edit]) {
4264
- min-height: var(--text-edit-min-height, 8rem);
4265
- max-height: var(--text-edit-max-height, none);
4253
+ position: relative;
4266
4254
  overflow-y: auto;
4267
- padding: var(--text-edit-padding, 0.75rem);
4268
- background: var(--text-edit-background, var(--color-surface-1));
4269
- border: 1px solid var(--color-line);
4270
- border-radius: var(--radius);
4255
+ max-height: var(--text-edit-max-height, none);
4271
4256
  outline: none;
4272
4257
 
4273
4258
  /* Page-level styling — set by .page controls, read back by $text.page.
@@ -4278,19 +4263,45 @@
4278
4263
  text-align: var(--text-edit-align, start);
4279
4264
  color: var(--text-edit-color, inherit);
4280
4265
 
4281
- &:focus {
4282
- border-color: var(--color-content-subtle);
4283
- }
4284
-
4285
4266
  /* Flagged from script: :empty never matches the <br> a contenteditable
4286
- keeps, and the tag holding it is not predictable after a wipe. */
4267
+ keeps, and the tag holding it is not predictable after a wipe. The
4268
+ placeholder OVERLAYS the first line (it is not a line of its own), so
4269
+ the caret sits on top of it and nothing jumps when typing starts. */
4287
4270
  &[data-text-edit-empty]::before {
4288
4271
  content: attr(placeholder);
4272
+ position: absolute;
4273
+ inset-block-start: var(--text-edit-padding, 0.75rem);
4274
+ inset-inline: var(--text-edit-padding, 0.75rem);
4289
4275
  color: var(--color-content-subtle);
4290
4276
  pointer-events: none;
4291
4277
  }
4292
4278
  }
4293
4279
 
4280
+ /* An unstyled area has no field padding, so its placeholder hugs the line. */
4281
+ :where([data-text-edit].unstyle)[data-text-edit-empty]::before {
4282
+ inset-block-start: 0;
4283
+ inset-inline: 0;
4284
+ }
4285
+
4286
+ /* The FIELD look. `.unstyle` drops it so an area can sit seamlessly in a
4287
+ page — a WYSIWYG document inherits its element's own type and colours;
4288
+ everything above (caret custody, placeholder, commands) still works. */
4289
+ :where([data-text-edit]:not(.unstyle)) {
4290
+ min-height: var(--text-edit-min-height, 8rem);
4291
+ padding: var(--text-edit-padding, 0.75rem);
4292
+ background: var(--text-edit-background, var(--color-surface-1));
4293
+ border: 1px solid var(--color-line);
4294
+ border-radius: var(--radius);
4295
+
4296
+ /* Held while the area owns the editing session — a <select> or colour
4297
+ control takes NATIVE focus by design, and the ring must not flicker
4298
+ off while the writer is mid-command. */
4299
+ &:focus,
4300
+ &[data-text-edit-focused] {
4301
+ border-color: var(--color-content-subtle);
4302
+ }
4303
+ }
4304
+
4294
4305
  /* ---- Selection menu (x-text-edit-menu) ----
4295
4306
  Positioned by the --text-edit-selection-* variables the plugin writes to
4296
4307
  :root, so the default needs no script; restyle or reposition freely. */
@@ -4301,7 +4312,6 @@
4301
4312
  left: var(--text-edit-selection-center);
4302
4313
  top: var(--text-edit-selection-y);
4303
4314
  transform: translate(-50%, calc(-100% - 0.5rem));
4304
- display: flex;
4305
4315
  gap: 0.25rem;
4306
4316
  align-items: center;
4307
4317
  padding: 0.25rem;
@@ -4311,6 +4321,22 @@
4311
4321
  box-shadow: 0 8px 30px rgb(0 0 0 / 0.2);
4312
4322
  }
4313
4323
 
4324
+ /* display only while OPEN: an author display on the base rule would beat
4325
+ the UA's closed-popover display:none, leaving the menu floating fixed
4326
+ over the page before any selection exists. */
4327
+ :where([data-text-edit-menu]:popover-open) {
4328
+ display: flex;
4329
+ }
4330
+
4331
+ /* Without the Popover API the attribute is inert and the menu would render
4332
+ permanently fixed on the page — hide it there; the bubble is progressive
4333
+ enhancement. (In supporting browsers a closed popover is hidden natively.) */
4334
+ @supports not selector(:popover-open) {
4335
+ :where([data-text-edit-menu]) {
4336
+ display: none;
4337
+ }
4338
+ }
4339
+
4314
4340
  /* ---- Controls ----
4315
4341
  A control is marked wherever it lives, so a toolbar is optional markup and
4316
4342
  never a requirement. The reset already dims [aria-disabled=true]. */
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.219';
5
+ const MANIFEST_BUILD_VERSION = '0.5.221';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -26,18 +26,18 @@
26
26
  itself; the affordance only has to be there when you go looking for it.
27
27
  While an inner affordance is engaged (text caret, handle, row), the region
28
28
  outline stands down so only the element being edited is highlighted. */
29
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *) {
29
+ :where([data-edit-armed][data-edit-authoring]):where([data-edit-ui], [data-edit-ui] *) {
30
30
  outline: 1px dashed transparent;
31
31
  outline-offset: 6px;
32
32
  border-radius: var(--radius);
33
33
  transition: outline-color 0.12s ease;
34
34
  }
35
35
 
36
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *):is(:hover, :focus-within):not(:has([contenteditable]:is(:hover, :focus), [data-edit-handle]:is(:hover, :focus), [data-edit-sortable]:is(:hover, :focus-within), [data-edit-movable]:is(:hover, :focus))) {
36
+ :where([data-edit-armed][data-edit-authoring]):where([data-edit-ui], [data-edit-ui] *):is(:hover, :focus-within):not(:has([contenteditable]:is(:hover, :focus), [data-edit-handle]:is(:hover, :focus), [data-edit-sortable]:is(:hover, :focus-within), [data-edit-movable]:is(:hover, :focus))) {
37
37
  outline-color: var(--color-line);
38
38
  }
39
39
 
40
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *)::before {
40
+ :where([data-edit-armed][data-edit-authoring]):where([data-edit-ui], [data-edit-ui] *)::before {
41
41
  content: attr(data-edit-label);
42
42
  position: absolute;
43
43
  inset-block-start: calc(-0.6em - 6px);
@@ -55,7 +55,7 @@
55
55
  transition: opacity 0.12s ease;
56
56
  }
57
57
 
58
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *):is(:hover, :focus-within):not(:has([contenteditable]:is(:hover, :focus), [data-edit-handle]:is(:hover, :focus), [data-edit-sortable]:is(:hover, :focus-within), [data-edit-movable]:is(:hover, :focus)))::before {
58
+ :where([data-edit-armed][data-edit-authoring]):where([data-edit-ui], [data-edit-ui] *):is(:hover, :focus-within):not(:has([contenteditable]:is(:hover, :focus), [data-edit-handle]:is(:hover, :focus), [data-edit-sortable]:is(:hover, :focus-within), [data-edit-movable]:is(:hover, :focus)))::before {
59
59
  opacity: 1;
60
60
  }
61
61
 
@@ -66,29 +66,9 @@
66
66
  transition: opacity 0.12s ease, outline-color 0.12s ease;
67
67
  }
68
68
 
69
- /* The grip: the plugin's own inline drag affordance, sitting inside the row
70
- at its inline start. A dot grid, revealed on approach. --edit-grip sizes
71
- it; `--edit-grip: 0` removes it (e.g. for grid rows where an extra flex
72
- item would take a cell). No full-row outline — the grip and the cursor
73
- say "draggable" without boxing the content. */
74
- :where([data-edit-ui] [data-edit-sortable], [data-edit-ui][data-edit-sortable])::before {
75
- content: '';
76
- align-self: center;
77
- flex: 0 0 auto;
78
- width: var(--edit-grip, 0.6rem);
79
- height: 1rem;
80
- margin-inline-end: calc(var(--edit-grip, 0.6rem) / 2);
81
- background-image: radial-gradient(circle, var(--color-content-subtle) 1px, transparent 1px);
82
- background-size: 0.3rem 0.3rem;
83
- background-position: center;
84
- opacity: 0;
85
- transition: opacity 0.12s ease;
86
- pointer-events: none;
87
- }
88
-
89
- :where([data-edit-ui] [data-edit-sortable], [data-edit-ui][data-edit-sortable]):is(:hover, :focus-visible)::before {
90
- opacity: 0.9;
91
- }
69
+ /* No injected grip: the row itself is the handle (grab cursor), while text
70
+ inside stays a text surface (text cursor, below). Rows that are pure text
71
+ still reorder by keyboard. */
92
72
 
93
73
  :where([data-edit-sortable], [data-edit-movable]):focus-visible {
94
74
  outline: 2px solid var(--edit-accent, var(--color-brand-content));
@@ -111,6 +91,14 @@
111
91
  pointer-events: none;
112
92
  }
113
93
 
94
+ /* Valid cross-region destinations during a drag: a dashed invitation, and
95
+ enough height that an EMPTY region can be dropped onto at all. */
96
+ :where([data-edit-drop-target]) {
97
+ outline: 1px dashed color-mix(in oklch, var(--edit-accent, var(--color-brand-content)) 55%, transparent);
98
+ outline-offset: 4px;
99
+ min-height: 2.5rem;
100
+ }
101
+
114
102
  /* Nothing else should react while a drag is in progress */
115
103
  :where([data-edit-dragging-in]) {
116
104
  cursor: grabbing;
@@ -142,6 +130,7 @@
142
130
  1px line, so focusing doesn't make the page jump a weight class. */
143
131
  :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"],
144
132
  :where([data-edit-ui])[data-edit-armed] [contenteditable="true"] {
133
+ cursor: text;
145
134
  outline: 1px solid transparent;
146
135
  outline-offset: 2px;
147
136
  border-radius: 3px;
@@ -17,7 +17,7 @@
17
17
 
18
18
  (function () {
19
19
  const LS_KEY = 'mnfst-edit-log';
20
- const SCHEMA = 6; // overlay schema version — bump when the delta shape changes
20
+ const SCHEMA = 7; // overlay schema version — bump when the delta shape changes
21
21
  const HISTORY_CAP = 400; // cap the append-only log so long sessions don't grow unbounded
22
22
  const ALL_CAPS = ['sort', 'text', 'style', 'size', 'data']; // 'data' = edit $x field values (opt-in)
23
23
  let dragged = null, autoN = 0;
@@ -367,7 +367,12 @@
367
367
  const region = key(area), path = el.getAttribute('data-edit-key');
368
368
  if (!path) return; // not an addressable node
369
369
  if (prop === 'text') value = sanitizeFor(el, value);
370
- const before = prop === 'text' ? el._preEdit : prop === 'class' ? el._preClass : el._preStyle;
370
+ // _pre* comes from the focus/open listener; when an edit arrives without
371
+ // one (programmatic focus, scripted writes), the arm-time baseline keeps
372
+ // `before` truthful so undo can still reach the original.
373
+ const fallback = prop === 'text' ? area._baseText && area._baseText[path] : prop === 'class' ? area._baseClass && area._baseClass[path] : area._baseStyle && area._baseStyle[path];
374
+ const pre = prop === 'text' ? el._preEdit : prop === 'class' ? el._preClass : el._preStyle;
375
+ const before = pre !== undefined ? pre : fallback;
371
376
  if (before === value) return;
372
377
  log.splice(cursor); log.push({ kind: 'st-node', region, path, prop, value, before, sig: nodeSig(el) }); cursor = log.length; saveState(); refresh();
373
378
  }
@@ -421,7 +426,7 @@
421
426
  if (el.hasAttribute('data-edit-handle') || el.hasAttribute('data-edit-ghost')) continue;
422
427
  if (el.parentElement && el.parentElement.closest('[data-text-edit]')) continue;
423
428
  const cur = el.getAttribute('data-edit-key');
424
- const base = cur ? cur.split('#')[0] : staticKey(el);
429
+ const base = cur ? ((/^([\s\S]*)#\d+$/.exec(cur) || [, cur])[1]) : staticKey(el); // ordinal = last #digits suffix
425
430
  const n = seen[base] = (seen[base] || 0) + 1;
426
431
  const k = n > 1 ? base + '#' + n : base;
427
432
  if (cur !== k) { el.setAttribute('data-edit-key', k); if (cur) changes.push([cur, k]); }
@@ -775,6 +780,12 @@
775
780
  // opens is a real element the author can style — by default a translucent
776
781
  // copy of what is being dragged, showing exactly where it would land.
777
782
  const lift = (ev) => {
783
+ // Valid cross-region destinations announce themselves for the whole
784
+ // drag: a dashed target outline, and a min-height (via CSS) so an
785
+ // EMPTY region has a surface to drop onto at all.
786
+ if (crossOK) areas().forEach(a => {
787
+ if (a !== area && isActive(a) && !locked(a) && capOf(a, 'sort') && classify(a) === 'static' && topArea(a)) a.setAttribute('data-edit-drop-target', '');
788
+ });
778
789
  ghost = item.cloneNode(true);
779
790
  ghost.setAttribute('data-edit-ghost', '');
780
791
  ghost.setAttribute('x-ignore', ''); // a clone must not re-bind
@@ -822,6 +833,7 @@
822
833
  const settle = (cancelled) => {
823
834
  document.removeEventListener('pointermove', onMove);
824
835
  document.removeEventListener('pointerup', onUp);
836
+ document.removeEventListener('pointercancel', onCancel);
825
837
  document.removeEventListener('keydown', onKey, true);
826
838
  if (frame) cancelAnimationFrame(frame);
827
839
  if (!active) return;
@@ -832,6 +844,7 @@
832
844
  if (preStyle == null) item.removeAttribute('style'); else item.setAttribute('style', preStyle);
833
845
  item.removeAttribute('data-edit-dragging');
834
846
  area.removeAttribute('data-edit-dragging-in');
847
+ document.querySelectorAll('[data-edit-drop-target]').forEach(a => a.removeAttribute('data-edit-drop-target'));
835
848
  if (hovered !== container) hovered.removeAttribute('data-edit-dragging-in');
836
849
  ghost = null; dragged = null;
837
850
  if (cancelled) { announce('Cancelled'); return; }
@@ -845,10 +858,14 @@
845
858
  else finishReorder(area);
846
859
  };
847
860
  const onUp = () => settle(false);
848
- // Escape puts it back, the way every drag is expected to be escapable.
861
+ // Escape puts it back, the way every drag is expected to be escapable. A
862
+ // cancelled pointer (OS gesture, alert, pen out of range) also reverts —
863
+ // otherwise the item wedges position:fixed with drop targets still lit.
864
+ const onCancel = () => settle(true);
849
865
  const onKey = (ev) => { if (ev.key !== 'Escape') return; ev.preventDefault(); ev.stopPropagation(); settle(true); };
850
866
  document.addEventListener('pointermove', onMove);
851
867
  document.addEventListener('pointerup', onUp);
868
+ document.addEventListener('pointercancel', onCancel);
852
869
  document.addEventListener('keydown', onKey, true);
853
870
  }
854
871
  function reorderOver(container, x, y) {
@@ -910,7 +927,9 @@
910
927
  const lu = unitOf(el.style.left) || 'px', tu = unitOf(el.style.top) || 'px';
911
928
  const baseL = parseFloat(cs.left) || 0, baseT = parseFloat(cs.top) || 0, sx = e.clientX, sy = e.clientY;
912
929
  const ov = showOverlay();
913
- const move = (ev) => { ev.preventDefault(); el.style.left = toUnit(baseL + (ev.clientX - sx), lu, el, parent, 'w') + lu; el.style.top = toUnit(baseT + (ev.clientY - sy), tu, el, parent, 'h') + tu; };
930
+ // Clamped to the containing block: a freeform card must not leave its canvas.
931
+ const maxL = () => Math.max(0, parent.clientWidth - el.offsetWidth), maxT = () => Math.max(0, parent.clientHeight - el.offsetHeight);
932
+ const move = (ev) => { ev.preventDefault(); el.style.left = toUnit(Math.min(maxL(), Math.max(0, baseL + (ev.clientX - sx))), lu, el, parent, 'w') + lu; el.style.top = toUnit(Math.min(maxT(), Math.max(0, baseT + (ev.clientY - sy))), tu, el, parent, 'h') + tu; };
914
933
  const up = () => { document.removeEventListener('pointermove', move); document.removeEventListener('pointerup', up); hideOverlay(ov); commitStyle(area, el); };
915
934
  document.addEventListener('pointermove', move); document.addEventListener('pointerup', up);
916
935
  });
@@ -921,8 +940,9 @@
921
940
  if (!el._moveTimer) el._preStyle = el.getAttribute('style') || ''; // baseline at start of a key burst
922
941
  const step = e.shiftKey ? 16 : 4, cs = getComputedStyle(el), parent = el.offsetParent || el.parentElement;
923
942
  const lu = unitOf(el.style.left) || 'px', tu = unitOf(el.style.top) || 'px';
924
- if (d[0]) el.style.left = toUnit((parseFloat(cs.left) || 0) + d[0] * step, lu, el, parent, 'w') + lu;
925
- if (d[1]) el.style.top = toUnit((parseFloat(cs.top) || 0) + d[1] * step, tu, el, parent, 'h') + tu;
943
+ const maxL = Math.max(0, parent.clientWidth - el.offsetWidth), maxT = Math.max(0, parent.clientHeight - el.offsetHeight);
944
+ if (d[0]) el.style.left = toUnit(Math.min(maxL, Math.max(0, (parseFloat(cs.left) || 0) + d[0] * step)), lu, el, parent, 'w') + lu;
945
+ if (d[1]) el.style.top = toUnit(Math.min(maxT, Math.max(0, (parseFloat(cs.top) || 0) + d[1] * step)), tu, el, parent, 'h') + tu;
926
946
  clearTimeout(el._moveTimer); el._moveTimer = setTimeout(() => { el._moveTimer = null; commitStyle(area, el); }, 350);
927
947
  });
928
948
  }
@@ -1042,6 +1062,7 @@
1042
1062
  // own caret placement; a second click still collapses to a caret for fine edits.
1043
1063
  function selectAllIn(el) {
1044
1064
  requestAnimationFrame(() => {
1065
+ if (dragged || grabbed) return; // reordering, not editing
1045
1066
  if (document.activeElement !== el) return;
1046
1067
  try { const r = document.createRange(); r.selectNodeContents(el); const s = window.getSelection(); s.removeAllRanges(); s.addRange(r); } catch { }
1047
1068
  });
@@ -1066,14 +1087,19 @@
1066
1087
  // STATIC: literal leaves only (skip bound nodes; those belong to data/component).
1067
1088
  function armText(area) {
1068
1089
  armRichText(area);
1069
- area.querySelectorAll('*').forEach(el => {
1070
- if (el.children.length || !el.textContent.trim() || el.closest('template') || el.hasAttribute('data-edit-handle')) return;
1090
+ // A region whose root IS the text — <p x-edit.text="note">…</p> — has no
1091
+ // descendant leaves; the area itself is the editable leaf.
1092
+ const nodes = [...area.querySelectorAll('*')];
1093
+ if (!realChildren(area).length) nodes.unshift(area); // size handles are children too
1094
+ nodes.forEach(el => {
1095
+ if (realChildren(el).length || !el.textContent.trim() || el.closest('template') || el.hasAttribute('data-edit-handle')) return;
1071
1096
  if (el.closest('[data-text-edit]')) return; // the rich editor owns this subtree
1072
1097
  if (!capOf(el, 'text') || el.hasAttribute('x-text') || el.hasAttribute('x-html')) return;
1073
1098
  el.setAttribute('contenteditable', 'true');
1074
1099
  if (el._textBound) return; el._textBound = true;
1075
- el.addEventListener('focus', () => { el._preEdit = el.innerHTML.trim(); const d = el.closest('[draggable="true"]'); if (d) d.setAttribute('draggable', 'false'); selectAllIn(el); });
1076
- el.addEventListener('blur', () => { const d = el.closest('[data-edit-area] [draggable="false"]'); if (d) d.setAttribute('draggable', 'true'); commitStaticNode(regionOf(el) || area, el, 'text', el.innerHTML.trim()); }); // live region: the element may have moved since arming
1100
+ el.addEventListener('focus', () => { el._preEdit = el.innerHTML.trim(); selectAllIn(el); });
1101
+ el.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); el.blur(); } }); // Escape finishes the edit, like the rich editor
1102
+ el.addEventListener('blur', () => commitStaticNode(regionOf(el) || area, el, 'text', el.innerHTML.trim())); // live region: the element may have moved since arming
1077
1103
  });
1078
1104
  }
1079
1105
 
@@ -1105,7 +1131,7 @@
1105
1131
  if (!el.hasAttribute('data-text-edit')) el.setAttribute('contenteditable', 'true');
1106
1132
  if (el._dvBound) return; el._dvBound = true;
1107
1133
  const read = () => rich ? el.innerHTML.trim() : el.textContent;
1108
- el.addEventListener('focus', () => { el._preEdit = read(); selectAllIn(el); });
1134
+ el.addEventListener('focus', () => { el._preEdit = read(); if (!el.hasAttribute('data-text-edit')) selectAllIn(el); }); // the rich editor owns its caret
1109
1135
  el.addEventListener('focusin', () => { el._preEdit = read(); });
1110
1136
  el.addEventListener('blur', () => commitDataValue(area, source, id, field, read(), el), true);
1111
1137
  });
@@ -1321,7 +1347,9 @@
1321
1347
  if (k) {
1322
1348
  // Keep the tally in step so a new element never claims an ordinal
1323
1349
  // an existing one already holds.
1324
- const [base, n] = k.split('#');
1350
+ // The ordinal is the LAST #digits suffix — authored text may contain '#'.
1351
+ const m = /^([\s\S]*)#(\d+)$/.exec(k);
1352
+ const base = m ? m[1] : k, n = m ? m[2] : undefined;
1325
1353
  seen[base] = Math.max(seen[base] || 0, n ? +n : 1);
1326
1354
  } else {
1327
1355
  const base = el.tagName + ':' + (el.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 24);
@@ -1332,6 +1360,7 @@
1332
1360
  el.setAttribute('data-edit-path', pathOf(el, area)); // still what the source writer navigates by
1333
1361
  if (!(k in area._baseClass)) area._baseClass[k] = el.getAttribute('class') || '';
1334
1362
  if (!(k in area._baseStyle)) area._baseStyle[k] = el.getAttribute('style') || '';
1363
+ if (!(k in area._baseText) && !el.children.length) area._baseText[k] = el.innerHTML.trim();
1335
1364
  };
1336
1365
  markEl(area);
1337
1366
  // Stop at a rich editor: its internals are its own, and marking them would
@@ -1360,6 +1389,15 @@
1360
1389
  [area, ...area.querySelectorAll('[data-edit-area]')].forEach(c => { if (capOf(c, 'sort') && !locked(c)) makeSortable(c); });
1361
1390
  [area, ...area.querySelectorAll('[data-edit-area]')].forEach(c => { if (ownsCap(c, 'size')) armSize(c); });
1362
1391
  if (kind === 'component') armComponent(area); else armText(area);
1392
+ if (!area._dndBound) {
1393
+ area._dndBound = true;
1394
+ // Native HTML5 drag (a text selection, an image, a link) must never
1395
+ // start inside a region: it drops SELECTED TEXT into neighbouring
1396
+ // editable leaves, silently merging elements. All region dragging is
1397
+ // pointer-based.
1398
+ area.addEventListener('dragstart', (e) => { if (isActive(area)) e.preventDefault(); });
1399
+ area.addEventListener('drop', (e) => { if (isActive(area)) e.preventDefault(); }); // nothing drops INTO a region either (an OS image drop would merge into a leaf)
1400
+ }
1363
1401
  if (!area._ctxBound) {
1364
1402
  area._ctxBound = true;
1365
1403
  // One binding for the whole area: report the block, and fall back to the
@@ -1,26 +1,26 @@
1
1
  {
2
2
  "manifest.appwrite.auth.js": "sha384-X30RFvbwT9qEIaMsMUumnW54ZzcnY5aY9QQeXPG8/0sI6VAbAw3+FZ6iuZb6b4cu",
3
- "manifest.appwrite.data.js": "sha384-K0TiCzUFBML9Vf/deb/hrzMGQVpN5OZRcMNA/oUIkvmsmuwnVs/Vl+CiEkIxBswM",
3
+ "manifest.appwrite.data.js": "sha384-oPSItIjZCJghmbtcE+6wx6pU3RL925Z4itl/YWGxSpreVY8E5gjF9jI6NXNDCVm6",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
5
  "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
6
6
  "manifest.charts.js": "sha384-gEAv/98+29RbhvNkiNWTNyBPbLTFr2Ld15wLu29FLCL57j1cwGChRrL9Pfk7dBtT",
7
7
  "manifest.chat.js": "sha384-88DfMfJLoi2oxclVD2iIMDkIGAExMXq5k47mekUredlicLaP3oZY5bwtwqs+TqDF",
8
- "manifest.code.js": "sha384-5Ki7YogsJza+eVpEU9L8wq/WwdBFIbXzrc3+TFgiDB9skA1V+EW9Tnse8gkxSyej",
8
+ "manifest.code.js": "sha384-uhE6Ha+pCgzSgBv5L6Ev7g1toOSSwBOiMAmvWVzkrikknFxkHEh5YKfrfFIxN3vw",
9
9
  "manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
10
10
  "manifest.colorpicker.js": "sha384-BhcCe8cGcd0DTJNwzXWWY+FQNRfSFFl6XqfHIFP8yHg/aXFks1NIC6lVx0Sr1VkM",
11
11
  "manifest.combobox.js": "sha384-MILTkrgsTzsYUjGo4ZuADqV85Xs4GmfB4xexfe9vbJMIoQTZ42W4zs+DTaVSPEsx",
12
12
  "manifest.components.js": "sha384-4bc3HgPvF9DHMiOG08w+nWXI7Hho01RuAPAUL9xyLxOiS2igeZpulCVcvr5aT9v6",
13
13
  "manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
14
- "manifest.data.js": "sha384-jXsslu3l0d7Sw2M3XEbiD8bBLM7bX1tzlqYCNgMJeKhIx3DGRl1UKNd2WKwKxfr9",
14
+ "manifest.data.js": "sha384-4ulI/WUv1NxDgdf7WzzffU2N0HcB0lQpyOr7PNFrhdkvVmOssh6j8RjwO3emZt7U",
15
15
  "manifest.datepicker.js": "sha384-XnbeURni2X91ozVzatQcSjYYIS4MoW12dBRJnYAGBr0nCQ624heH5s+xtQSpLZX6",
16
16
  "manifest.defer.js": "sha384-WcCf8YQ1bKIXCLSZ24a0bJwepbYntYc8TzQM50hklo5VMIH8AtzDnwl6WU4prRv1",
17
17
  "manifest.device.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
18
18
  "manifest.dropdowns.js": "sha384-0ilj9AvRph0LacIBJSqwpu1fvPRn5gcWbhIG1SOpaoM/Q8idMbKBLupwM+5O0UaX",
19
- "manifest.edit.js": "sha384-X3FVmNQvDMV8RqVYxAt1TDb6BTsf/qiWOPnFEM1L67m1LpeW7K+m9mCgOO8TsFPB",
19
+ "manifest.edit.js": "sha384-63ym+1EWz+JKvwwH3v2XlR4q5alha/ShMiIJsbDbDS7eVymu5zgghsh8JRVeax+V",
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
- "manifest.markdown.js": "sha384-Wix51VpAtzCPo43dM6eHa633+W/Xtn/xRN4QBSK8Hx7s4fP1kequ8ecaSMyacNvG",
23
+ "manifest.markdown.js": "sha384-JWUxlUZADxk1hZY7NpDX051UhtXt/ofpfsCDBRcweU6JhLbN8B832yPLgCatxFdl",
24
24
  "manifest.native.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
25
25
  "manifest.payments.js": "sha384-8pofk2zhxtapPMpwnFyTelPMrYhShNhDCE722uQh/EYFvZFuPLmSdgunqsarVQjv",
26
26
  "manifest.resize.js": "sha384-+0IsyrGP/4gZmNbD6oU8dCgEyhpBsXMm74NIenYeUGuGRWyJDurcG27xE/CA0/aA",
@@ -28,10 +28,10 @@
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-ZHAcZayIy8b/fuajad/c6RA3zNLMjxb7Gw+JAIKJz/N5PvFBvk9M8o1XOAMWW9da",
31
+ "manifest.sw.js": "sha384-oQvga1NtT0DZ6GU0aoy/zA1OAZIZc03cCpiEMOSA3z5S2lwR+QxcmJjpQqgxDKX/",
32
32
  "manifest.tabs.js": "sha384-OIIcyaQnGS0eNYk4zgAFEDboNYect7THvUd9D0V6nqCeDOxQcoiqLrBEmmWVTTet",
33
33
  "manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
34
- "manifest.text.edit.js": "sha384-AZsUx2nwCoEUl1SxFkuf6WjskKc70ve20tsS47oVxlGSRH+Zy0ICEx5cCMY2n7d8",
34
+ "manifest.text.edit.js": "sha384-RzOvLQE8GbmI6gntAwiotDewYT/08OHcd7sJ5hLon8e8u1Xbq1D7VfEGuaRVd2CE",
35
35
  "manifest.toasts.js": "sha384-/OV32LLMJl0Iw8NoNQJ+5lgntLxjIJq/YNj5JAKS/+nUhVx4MMB9h1FOqP7zuj2M",
36
36
  "manifest.tooltips.js": "sha384-uXVVQADl6pY2Pj9i/UaSclZN7dzbKhLkmevv7Ad0+wXR8LgqodALNFrc5jZJjlwm",
37
37
  "manifest.url.parameters.js": "sha384-J2aSVBOPsoqsbafHndrWLHEDpKb4AKJAoQnVmKA4gZh5P/8HHjFWHO/lkusmG84l",
@@ -645,15 +645,21 @@ async function initializeMarkdownPlugin() {
645
645
 
646
646
  // Track last processed content to prevent unnecessary re-renders
647
647
  let lastProcessedContent = null;
648
+ // Baked output is protected from empty-value hides only until the
649
+ // first live render replaces it; after that, clearing the value
650
+ // hides the element like it always did.
651
+ let bakedOnly = hasBakedContent;
648
652
 
649
653
  effect(() => {
650
654
  getMarkdownContent(async (pathOrContent) => {
651
655
  const mdDone = mdTrack();
652
656
  try {
653
- // Reset visibility if content is empty/undefined
657
+ // Reset visibility if content is empty/undefined. Baked
658
+ // content stays visible: a reactive expression can evaluate
659
+ // empty at boot (data still loading), and hiding then would
660
+ // blank prerendered output.
654
661
  if (!pathOrContent || pathOrContent === undefined || pathOrContent === '') {
655
- el.style.opacity = '0';
656
- hasContent = false;
662
+ if (!bakedOnly) { el.style.opacity = '0'; hasContent = false; }
657
663
  return;
658
664
  }
659
665
 
@@ -726,8 +732,7 @@ async function initializeMarkdownPlugin() {
726
732
  // Ensure we have a string (e.g. $route('path')?.content can be a proxy while loading)
727
733
  const contentStr = typeof markdownContent === 'string' ? markdownContent : '';
728
734
  if (!contentStr || contentStr.trim() === '') {
729
- el.style.opacity = '0';
730
- hasContent = false;
735
+ if (!bakedOnly) { el.style.opacity = '0'; hasContent = false; }
731
736
  return;
732
737
  }
733
738
 
@@ -770,6 +775,7 @@ async function initializeMarkdownPlugin() {
770
775
 
771
776
  // Show content with fade-in
772
777
  hasContent = true;
778
+ bakedOnly = false;
773
779
  el.style.opacity = '1';
774
780
 
775
781
  // Extract headings for anchor links