mnfst 0.5.220 → 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.220';
5
+ const MANIFEST_BUILD_VERSION = '0.5.221';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
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,31 +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-display: none` removes it entirely (a zero width alone
2992
- would still occupy a grid cell). No full-row outline — the grip and the
2993
- cursor say "draggable" without boxing the content. */
2994
- :where([data-edit-ui] [data-edit-sortable], [data-edit-ui][data-edit-sortable])::before {
2995
- content: '';
2996
- display: var(--edit-grip-display, inline-block);
2997
- vertical-align: middle;
2998
- align-self: center;
2999
- flex: 0 0 auto;
3000
- width: var(--edit-grip, 0.6rem);
3001
- height: 1rem;
3002
- margin-inline-end: calc(var(--edit-grip, 0.6rem) / 2);
3003
- background-image: radial-gradient(circle, var(--color-content-subtle) 1px, transparent 1px);
3004
- background-size: 0.3rem 0.3rem;
3005
- background-position: center;
3006
- opacity: 0;
3007
- transition: opacity 0.12s ease;
3008
- pointer-events: none;
3009
- }
3010
-
3011
- :where([data-edit-ui] [data-edit-sortable], [data-edit-ui][data-edit-sortable]):is(:hover, :focus-visible)::before {
3012
- opacity: 0.9;
3013
- }
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. */
3014
2992
 
3015
2993
  :where([data-edit-sortable], [data-edit-movable]):focus-visible {
3016
2994
  outline: 2px solid var(--edit-accent, var(--color-brand-content));
@@ -3033,6 +3011,14 @@
3033
3011
  pointer-events: none;
3034
3012
  }
3035
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
+
3036
3022
  /* Nothing else should react while a drag is in progress */
3037
3023
  :where([data-edit-dragging-in]) {
3038
3024
  cursor: grabbing;
@@ -3064,6 +3050,7 @@
3064
3050
  1px line, so focusing doesn't make the page jump a weight class. */
3065
3051
  :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"],
3066
3052
  :where([data-edit-ui])[data-edit-armed] [contenteditable="true"] {
3053
+ cursor: text;
3067
3054
  outline: 1px solid transparent;
3068
3055
  outline-offset: 2px;
3069
3056
  border-radius: 3px;
@@ -4263,13 +4250,9 @@
4263
4250
  @layer components {
4264
4251
 
4265
4252
  :where([data-text-edit]) {
4266
- min-height: var(--text-edit-min-height, 8rem);
4267
- max-height: var(--text-edit-max-height, none);
4253
+ position: relative;
4268
4254
  overflow-y: auto;
4269
- padding: var(--text-edit-padding, 0.75rem);
4270
- background: var(--text-edit-background, var(--color-surface-1));
4271
- border: 1px solid var(--color-line);
4272
- border-radius: var(--radius);
4255
+ max-height: var(--text-edit-max-height, none);
4273
4256
  outline: none;
4274
4257
 
4275
4258
  /* Page-level styling — set by .page controls, read back by $text.page.
@@ -4280,19 +4263,45 @@
4280
4263
  text-align: var(--text-edit-align, start);
4281
4264
  color: var(--text-edit-color, inherit);
4282
4265
 
4283
- &:focus {
4284
- border-color: var(--color-content-subtle);
4285
- }
4286
-
4287
4266
  /* Flagged from script: :empty never matches the <br> a contenteditable
4288
- 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. */
4289
4270
  &[data-text-edit-empty]::before {
4290
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);
4291
4275
  color: var(--color-content-subtle);
4292
4276
  pointer-events: none;
4293
4277
  }
4294
4278
  }
4295
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
+
4296
4305
  /* ---- Selection menu (x-text-edit-menu) ----
4297
4306
  Positioned by the --text-edit-selection-* variables the plugin writes to
4298
4307
  :root, so the default needs no script; restyle or reposition freely. */
@@ -4303,7 +4312,6 @@
4303
4312
  left: var(--text-edit-selection-center);
4304
4313
  top: var(--text-edit-selection-y);
4305
4314
  transform: translate(-50%, calc(-100% - 0.5rem));
4306
- display: flex;
4307
4315
  gap: 0.25rem;
4308
4316
  align-items: center;
4309
4317
  padding: 0.25rem;
@@ -4313,6 +4321,13 @@
4313
4321
  box-shadow: 0 8px 30px rgb(0 0 0 / 0.2);
4314
4322
  }
4315
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
+
4316
4331
  /* Without the Popover API the attribute is inert and the menu would render
4317
4332
  permanently fixed on the page — hide it there; the bubble is progressive
4318
4333
  enhancement. (In supporting browsers a closed popover is hidden natively.) */
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.220';
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,31 +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-display: none` removes it entirely (a zero width alone
72
- would still occupy a grid cell). No full-row outline — the grip and the
73
- cursor say "draggable" without boxing the content. */
74
- :where([data-edit-ui] [data-edit-sortable], [data-edit-ui][data-edit-sortable])::before {
75
- content: '';
76
- display: var(--edit-grip-display, inline-block);
77
- vertical-align: middle;
78
- align-self: center;
79
- flex: 0 0 auto;
80
- width: var(--edit-grip, 0.6rem);
81
- height: 1rem;
82
- margin-inline-end: calc(var(--edit-grip, 0.6rem) / 2);
83
- background-image: radial-gradient(circle, var(--color-content-subtle) 1px, transparent 1px);
84
- background-size: 0.3rem 0.3rem;
85
- background-position: center;
86
- opacity: 0;
87
- transition: opacity 0.12s ease;
88
- pointer-events: none;
89
- }
90
-
91
- :where([data-edit-ui] [data-edit-sortable], [data-edit-ui][data-edit-sortable]):is(:hover, :focus-visible)::before {
92
- opacity: 0.9;
93
- }
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. */
94
72
 
95
73
  :where([data-edit-sortable], [data-edit-movable]):focus-visible {
96
74
  outline: 2px solid var(--edit-accent, var(--color-brand-content));
@@ -113,6 +91,14 @@
113
91
  pointer-events: none;
114
92
  }
115
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
+
116
102
  /* Nothing else should react while a drag is in progress */
117
103
  :where([data-edit-dragging-in]) {
118
104
  cursor: grabbing;
@@ -144,6 +130,7 @@
144
130
  1px line, so focusing doesn't make the page jump a weight class. */
145
131
  :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"],
146
132
  :where([data-edit-ui])[data-edit-armed] [contenteditable="true"] {
133
+ cursor: text;
147
134
  outline: 1px solid transparent;
148
135
  outline-offset: 2px;
149
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
 
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "manifest.appwrite.auth.js": "sha384-X30RFvbwT9qEIaMsMUumnW54ZzcnY5aY9QQeXPG8/0sI6VAbAw3+FZ6iuZb6b4cu",
3
- "manifest.appwrite.data.js": "sha384-Jkz5STB1ohrl5QaEQwCd2YMK6O4pP9byt+0WjJEHTKGKBZhm3HhKTFLh41TeGrtP",
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",
@@ -11,12 +11,12 @@
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-k0e3ledccuEmpafn1D3VhSRjUi4LMru+0n434LfInWUxthmbUFTgeGBwJgCxE1fs",
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-vlFa+vfuiz5ff/md0JDQ16NuAq6ekggHgxbO+QG2ugMAvwTH055Z20mwuIr9BtNR",
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",
@@ -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-ImJzl1bWd3CdD0Z9L9+6wlU0WevaZnYJ7florX4kagSvDkfWs4LmjvOB/YkbRcI3",
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-qsph2bNsRnH8ucy9qmaNs9MsWbA6yMm21zgfNIxcC3eLG+PIJfhSfLygfzuDJ0IF",
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",