mnfst 0.5.220 → 0.5.222

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.222';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
package/lib/manifest.css CHANGED
@@ -2930,54 +2930,22 @@
2930
2930
  @layer utilities {
2931
2931
 
2932
2932
  /* ---- Editing UI scope ----
2933
- ALL chrome — region outline + label, sortable grips, movable/text outlines,
2934
- size-handle hairlines, the floating toolbar — lives under ONE scope:
2935
- [data-edit-ui], on the region itself or any ancestor (e.g. <body data-edit-ui>
2936
- for a whole editor view). The plugin sets it on `.authoring` regions
2937
- automatically; everything else opts in with the attribute. A region outside
2938
- the scope is just as editable — it simply carries no visual cues. */
2933
+ ALL chrome — cursors, movable/text outlines, size-handle hairlines, drop
2934
+ targets, the floating toolbar — lives under ONE scope: [data-edit-ui], on
2935
+ the region itself or any ancestor (e.g. <body data-edit-ui> for a whole
2936
+ editor view). The plugin sets it on `.authoring` regions automatically;
2937
+ everything else opts in with the attribute. A region outside the scope is
2938
+ just as editable — it simply carries no visual cues. */
2939
2939
 
2940
2940
  /* ---- Editable area (armed) ---- */
2941
2941
  :where([data-edit-armed]) {
2942
2942
  position: relative;
2943
2943
  }
2944
2944
 
2945
- /* Quiet until touched. A page covered in dashed boxes reads as a diagram of
2946
- itself; the affordance only has to be there when you go looking for it.
2947
- While an inner affordance is engaged (text caret, handle, row), the region
2948
- outline stands down so only the element being edited is highlighted. */
2949
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *) {
2950
- outline: 1px dashed transparent;
2951
- outline-offset: 6px;
2952
- border-radius: var(--radius);
2953
- transition: outline-color 0.12s ease;
2954
- }
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))) {
2957
- outline-color: var(--color-line);
2958
- }
2959
-
2960
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *)::before {
2961
- content: attr(data-edit-label);
2962
- position: absolute;
2963
- inset-block-start: calc(-0.6em - 6px);
2964
- inset-inline-start: 0.5rem;
2965
- padding-inline: 0.3rem;
2966
- background: var(--color-page);
2967
- border-radius: 3px;
2968
- font-size: 0.7rem;
2969
- line-height: 1.2;
2970
- letter-spacing: 0.04em;
2971
- text-transform: uppercase;
2972
- color: var(--color-content-subtle);
2973
- pointer-events: none;
2974
- opacity: 0;
2975
- transition: opacity 0.12s ease;
2976
- }
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 {
2979
- opacity: 1;
2980
- }
2945
+ /* No default region boundary or label: a page being edited should look
2946
+ like the page, not a diagram of its regions. The hooks remain for a real
2947
+ editor product to draw its own — [data-edit-authoring] marks the region
2948
+ and data-edit-label carries "key · regime". */
2981
2949
 
2982
2950
  /* ---- Reorder (pointer + keyboard; touch-action:none lets touch-drag beat scroll) ---- */
2983
2951
  :where([data-edit-sortable]) {
@@ -2986,31 +2954,9 @@
2986
2954
  transition: opacity 0.12s ease, outline-color 0.12s ease;
2987
2955
  }
2988
2956
 
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
- }
2957
+ /* No injected grip: the row itself is the handle (grab cursor), while text
2958
+ inside stays a text surface (text cursor, below). Rows that are pure text
2959
+ still reorder by keyboard. */
3014
2960
 
3015
2961
  :where([data-edit-sortable], [data-edit-movable]):focus-visible {
3016
2962
  outline: 2px solid var(--edit-accent, var(--color-brand-content));
@@ -3033,6 +2979,14 @@
3033
2979
  pointer-events: none;
3034
2980
  }
3035
2981
 
2982
+ /* Valid cross-region destinations during a drag: a dashed invitation, and
2983
+ enough height that an EMPTY region can be dropped onto at all. */
2984
+ :where([data-edit-drop-target]) {
2985
+ outline: 1px dashed color-mix(in oklch, var(--edit-accent, var(--color-brand-content)) 55%, transparent);
2986
+ outline-offset: 4px;
2987
+ min-height: 2.5rem;
2988
+ }
2989
+
3036
2990
  /* Nothing else should react while a drag is in progress */
3037
2991
  :where([data-edit-dragging-in]) {
3038
2992
  cursor: grabbing;
@@ -3064,6 +3018,7 @@
3064
3018
  1px line, so focusing doesn't make the page jump a weight class. */
3065
3019
  :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"],
3066
3020
  :where([data-edit-ui])[data-edit-armed] [contenteditable="true"] {
3021
+ cursor: text;
3067
3022
  outline: 1px solid transparent;
3068
3023
  outline-offset: 2px;
3069
3024
  border-radius: 3px;
@@ -4263,13 +4218,9 @@
4263
4218
  @layer components {
4264
4219
 
4265
4220
  :where([data-text-edit]) {
4266
- min-height: var(--text-edit-min-height, 8rem);
4267
- max-height: var(--text-edit-max-height, none);
4221
+ position: relative;
4268
4222
  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);
4223
+ max-height: var(--text-edit-max-height, none);
4273
4224
  outline: none;
4274
4225
 
4275
4226
  /* Page-level styling — set by .page controls, read back by $text.page.
@@ -4280,19 +4231,45 @@
4280
4231
  text-align: var(--text-edit-align, start);
4281
4232
  color: var(--text-edit-color, inherit);
4282
4233
 
4283
- &:focus {
4284
- border-color: var(--color-content-subtle);
4285
- }
4286
-
4287
4234
  /* Flagged from script: :empty never matches the <br> a contenteditable
4288
- keeps, and the tag holding it is not predictable after a wipe. */
4235
+ keeps, and the tag holding it is not predictable after a wipe. The
4236
+ placeholder OVERLAYS the first line (it is not a line of its own), so
4237
+ the caret sits on top of it and nothing jumps when typing starts. */
4289
4238
  &[data-text-edit-empty]::before {
4290
4239
  content: attr(placeholder);
4240
+ position: absolute;
4241
+ inset-block-start: var(--text-edit-padding, 0.75rem);
4242
+ inset-inline: var(--text-edit-padding, 0.75rem);
4291
4243
  color: var(--color-content-subtle);
4292
4244
  pointer-events: none;
4293
4245
  }
4294
4246
  }
4295
4247
 
4248
+ /* An unstyled area has no field padding, so its placeholder hugs the line. */
4249
+ :where([data-text-edit].unstyle)[data-text-edit-empty]::before {
4250
+ inset-block-start: 0;
4251
+ inset-inline: 0;
4252
+ }
4253
+
4254
+ /* The FIELD look. `.unstyle` drops it so an area can sit seamlessly in a
4255
+ page — a WYSIWYG document inherits its element's own type and colours;
4256
+ everything above (caret custody, placeholder, commands) still works. */
4257
+ :where([data-text-edit]:not(.unstyle)) {
4258
+ min-height: var(--text-edit-min-height, 8rem);
4259
+ padding: var(--text-edit-padding, 0.75rem);
4260
+ background: var(--text-edit-background, var(--color-surface-1));
4261
+ border: 1px solid var(--color-line);
4262
+ border-radius: var(--radius);
4263
+
4264
+ /* Held while the area owns the editing session — a <select> or colour
4265
+ control takes NATIVE focus by design, and the ring must not flicker
4266
+ off while the writer is mid-command. */
4267
+ &:focus,
4268
+ &[data-text-edit-focused] {
4269
+ border-color: var(--color-content-subtle);
4270
+ }
4271
+ }
4272
+
4296
4273
  /* ---- Selection menu (x-text-edit-menu) ----
4297
4274
  Positioned by the --text-edit-selection-* variables the plugin writes to
4298
4275
  :root, so the default needs no script; restyle or reposition freely. */
@@ -4303,7 +4280,6 @@
4303
4280
  left: var(--text-edit-selection-center);
4304
4281
  top: var(--text-edit-selection-y);
4305
4282
  transform: translate(-50%, calc(-100% - 0.5rem));
4306
- display: flex;
4307
4283
  gap: 0.25rem;
4308
4284
  align-items: center;
4309
4285
  padding: 0.25rem;
@@ -4313,6 +4289,13 @@
4313
4289
  box-shadow: 0 8px 30px rgb(0 0 0 / 0.2);
4314
4290
  }
4315
4291
 
4292
+ /* display only while OPEN: an author display on the base rule would beat
4293
+ the UA's closed-popover display:none, leaving the menu floating fixed
4294
+ over the page before any selection exists. */
4295
+ :where([data-text-edit-menu]:popover-open) {
4296
+ display: flex;
4297
+ }
4298
+
4316
4299
  /* Without the Popover API the attribute is inert and the menu would render
4317
4300
  permanently fixed on the page — hide it there; the bubble is progressive
4318
4301
  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.222';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -10,54 +10,22 @@
10
10
  @layer utilities {
11
11
 
12
12
  /* ---- Editing UI scope ----
13
- ALL chrome — region outline + label, sortable grips, movable/text outlines,
14
- size-handle hairlines, the floating toolbar — lives under ONE scope:
15
- [data-edit-ui], on the region itself or any ancestor (e.g. <body data-edit-ui>
16
- for a whole editor view). The plugin sets it on `.authoring` regions
17
- automatically; everything else opts in with the attribute. A region outside
18
- the scope is just as editable — it simply carries no visual cues. */
13
+ ALL chrome — cursors, movable/text outlines, size-handle hairlines, drop
14
+ targets, the floating toolbar — lives under ONE scope: [data-edit-ui], on
15
+ the region itself or any ancestor (e.g. <body data-edit-ui> for a whole
16
+ editor view). The plugin sets it on `.authoring` regions automatically;
17
+ everything else opts in with the attribute. A region outside the scope is
18
+ just as editable — it simply carries no visual cues. */
19
19
 
20
20
  /* ---- Editable area (armed) ---- */
21
21
  :where([data-edit-armed]) {
22
22
  position: relative;
23
23
  }
24
24
 
25
- /* Quiet until touched. A page covered in dashed boxes reads as a diagram of
26
- itself; the affordance only has to be there when you go looking for it.
27
- While an inner affordance is engaged (text caret, handle, row), the region
28
- outline stands down so only the element being edited is highlighted. */
29
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *) {
30
- outline: 1px dashed transparent;
31
- outline-offset: 6px;
32
- border-radius: var(--radius);
33
- transition: outline-color 0.12s ease;
34
- }
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))) {
37
- outline-color: var(--color-line);
38
- }
39
-
40
- :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *)::before {
41
- content: attr(data-edit-label);
42
- position: absolute;
43
- inset-block-start: calc(-0.6em - 6px);
44
- inset-inline-start: 0.5rem;
45
- padding-inline: 0.3rem;
46
- background: var(--color-page);
47
- border-radius: 3px;
48
- font-size: 0.7rem;
49
- line-height: 1.2;
50
- letter-spacing: 0.04em;
51
- text-transform: uppercase;
52
- color: var(--color-content-subtle);
53
- pointer-events: none;
54
- opacity: 0;
55
- transition: opacity 0.12s ease;
56
- }
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 {
59
- opacity: 1;
60
- }
25
+ /* No default region boundary or label: a page being edited should look
26
+ like the page, not a diagram of its regions. The hooks remain for a real
27
+ editor product to draw its own — [data-edit-authoring] marks the region
28
+ and data-edit-label carries "key · regime". */
61
29
 
62
30
  /* ---- Reorder (pointer + keyboard; touch-action:none lets touch-drag beat scroll) ---- */
63
31
  :where([data-edit-sortable]) {
@@ -66,31 +34,9 @@
66
34
  transition: opacity 0.12s ease, outline-color 0.12s ease;
67
35
  }
68
36
 
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
- }
37
+ /* No injected grip: the row itself is the handle (grab cursor), while text
38
+ inside stays a text surface (text cursor, below). Rows that are pure text
39
+ still reorder by keyboard. */
94
40
 
95
41
  :where([data-edit-sortable], [data-edit-movable]):focus-visible {
96
42
  outline: 2px solid var(--edit-accent, var(--color-brand-content));
@@ -113,6 +59,14 @@
113
59
  pointer-events: none;
114
60
  }
115
61
 
62
+ /* Valid cross-region destinations during a drag: a dashed invitation, and
63
+ enough height that an EMPTY region can be dropped onto at all. */
64
+ :where([data-edit-drop-target]) {
65
+ outline: 1px dashed color-mix(in oklch, var(--edit-accent, var(--color-brand-content)) 55%, transparent);
66
+ outline-offset: 4px;
67
+ min-height: 2.5rem;
68
+ }
69
+
116
70
  /* Nothing else should react while a drag is in progress */
117
71
  :where([data-edit-dragging-in]) {
118
72
  cursor: grabbing;
@@ -144,6 +98,7 @@
144
98
  1px line, so focusing doesn't make the page jump a weight class. */
145
99
  :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"],
146
100
  :where([data-edit-ui])[data-edit-armed] [contenteditable="true"] {
101
+ cursor: text;
147
102
  outline: 1px solid transparent;
148
103
  outline-offset: 2px;
149
104
  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-dSnLE73aoJkrLXQ3EYSNgmMLJ8WZn6dGcZOKx6bY007Qnl/vY2JK75q/leiZXe7s",
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-ixpU7pvJhlPBvT7in0HonHQLdNOabbpqik6IVsMNNzAY3UhCVAf8BRno2niTkZK1",
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-P5M9kT06bquAyIK8aLQK6ujNn5rnaJ2Lw8f/ZsDx1yLVPWYeIOwa6Y06eEWmc5ry",
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",