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.
@@ -9,13 +9,9 @@
9
9
  @layer components {
10
10
 
11
11
  :where([data-text-edit]) {
12
- min-height: var(--text-edit-min-height, 8rem);
13
- max-height: var(--text-edit-max-height, none);
12
+ position: relative;
14
13
  overflow-y: auto;
15
- padding: var(--text-edit-padding, 0.75rem);
16
- background: var(--text-edit-background, var(--color-surface-1));
17
- border: 1px solid var(--color-line);
18
- border-radius: var(--radius);
14
+ max-height: var(--text-edit-max-height, none);
19
15
  outline: none;
20
16
 
21
17
  /* Page-level styling — set by .page controls, read back by $text.page.
@@ -26,19 +22,45 @@
26
22
  text-align: var(--text-edit-align, start);
27
23
  color: var(--text-edit-color, inherit);
28
24
 
29
- &:focus {
30
- border-color: var(--color-content-subtle);
31
- }
32
-
33
25
  /* Flagged from script: :empty never matches the <br> a contenteditable
34
- keeps, and the tag holding it is not predictable after a wipe. */
26
+ keeps, and the tag holding it is not predictable after a wipe. The
27
+ placeholder OVERLAYS the first line (it is not a line of its own), so
28
+ the caret sits on top of it and nothing jumps when typing starts. */
35
29
  &[data-text-edit-empty]::before {
36
30
  content: attr(placeholder);
31
+ position: absolute;
32
+ inset-block-start: var(--text-edit-padding, 0.75rem);
33
+ inset-inline: var(--text-edit-padding, 0.75rem);
37
34
  color: var(--color-content-subtle);
38
35
  pointer-events: none;
39
36
  }
40
37
  }
41
38
 
39
+ /* An unstyled area has no field padding, so its placeholder hugs the line. */
40
+ :where([data-text-edit].unstyle)[data-text-edit-empty]::before {
41
+ inset-block-start: 0;
42
+ inset-inline: 0;
43
+ }
44
+
45
+ /* The FIELD look. `.unstyle` drops it so an area can sit seamlessly in a
46
+ page — a WYSIWYG document inherits its element's own type and colours;
47
+ everything above (caret custody, placeholder, commands) still works. */
48
+ :where([data-text-edit]:not(.unstyle)) {
49
+ min-height: var(--text-edit-min-height, 8rem);
50
+ padding: var(--text-edit-padding, 0.75rem);
51
+ background: var(--text-edit-background, var(--color-surface-1));
52
+ border: 1px solid var(--color-line);
53
+ border-radius: var(--radius);
54
+
55
+ /* Held while the area owns the editing session — a <select> or colour
56
+ control takes NATIVE focus by design, and the ring must not flicker
57
+ off while the writer is mid-command. */
58
+ &:focus,
59
+ &[data-text-edit-focused] {
60
+ border-color: var(--color-content-subtle);
61
+ }
62
+ }
63
+
42
64
  /* ---- Selection menu (x-text-edit-menu) ----
43
65
  Positioned by the --text-edit-selection-* variables the plugin writes to
44
66
  :root, so the default needs no script; restyle or reposition freely. */
@@ -49,7 +71,6 @@
49
71
  left: var(--text-edit-selection-center);
50
72
  top: var(--text-edit-selection-y);
51
73
  transform: translate(-50%, calc(-100% - 0.5rem));
52
- display: flex;
53
74
  gap: 0.25rem;
54
75
  align-items: center;
55
76
  padding: 0.25rem;
@@ -59,6 +80,13 @@
59
80
  box-shadow: 0 8px 30px rgb(0 0 0 / 0.2);
60
81
  }
61
82
 
83
+ /* display only while OPEN: an author display on the base rule would beat
84
+ the UA's closed-popover display:none, leaving the menu floating fixed
85
+ over the page before any selection exists. */
86
+ :where([data-text-edit-menu]:popover-open) {
87
+ display: flex;
88
+ }
89
+
62
90
  /* Without the Popover API the attribute is inert and the menu would render
63
91
  permanently fixed on the page — hide it there; the bubble is progressive
64
92
  enhancement. (In supporting browsers a closed popover is hidden natively.) */
@@ -389,15 +389,34 @@
389
389
  Swapping the element outright covers every block tag, where formatBlock only
390
390
  accepts a short list and disagrees between engines. */
391
391
  function setBlock(tag) {
392
- const b = blockEl(); if (!b) return;
392
+ let b = blockEl(); if (!b) return;
393
393
  const want = (b.tagName === tag.toUpperCase() ? 'p' : tag).toLowerCase();
394
394
  if (b.tagName === want.toUpperCase()) return;
395
+ // A heading or quote cannot live inside a list — markdown has no
396
+ // <ul><h2> — so lift the item out of its list first, then retag.
397
+ // Retagging in place serialized to nothing: the content vanished.
398
+ // A NESTED item outdents to the top level first, or the lift would
399
+ // land the new block inside the outer <li>.
400
+ if (b.tagName === 'LI' && want !== 'li') {
401
+ const area = b.closest('[data-text-edit]');
402
+ for (let guard = 0; guard < 8; guard++) {
403
+ const list = b && b.tagName === 'LI' && b.closest('ul, ol');
404
+ if (!list || !list.parentElement || !list.parentElement.closest('li')) break;
405
+ indent(-1, area);
406
+ b = blockEl(); if (!b) return;
407
+ }
408
+ if (b.tagName === 'LI') { unwrapItem(b); b = blockEl(); if (!b) return; }
409
+ if (b.tagName === want.toUpperCase()) return;
410
+ }
395
411
  const n = document.createElement(want);
396
412
  n.setAttribute('style', b.getAttribute('style') || '');
397
413
  if (!n.getAttribute('style')) n.removeAttribute('style');
398
414
  const off = offsetIn(b);
399
415
  while (b.firstChild) n.appendChild(b.firstChild);
400
416
  b.replaceWith(n);
417
+ // A sublist that rode along out of a list item cannot live inside a
418
+ // heading or quote — it steps out right after the new block.
419
+ [...n.querySelectorAll(':scope > ul, :scope > ol')].reverse().forEach(l => n.after(l));
401
420
  caretIn(n, off);
402
421
  }
403
422
 
@@ -1641,6 +1660,7 @@
1641
1660
  const was = area || lastFocused;
1642
1661
  if (!area || area === lastFocused) lastFocused = null;
1643
1662
  if (!was) return;
1663
+ was.removeAttribute('data-text-edit-focused');
1644
1664
  was._selection = null;
1645
1665
  was.removeAttribute('data-text-edit-selected');
1646
1666
  document.documentElement.removeAttribute('data-text-edit-selected');
@@ -1697,6 +1717,13 @@
1697
1717
  const spec = COMMANDS[id], cfg = area && area._te;
1698
1718
  if (!spec || !cfg || cfg.mode === 'plain') return false;
1699
1719
  const page = !!(ctrl && ctrl.page && spec.page);
1720
+ // The writer's caret is visibly in ANOTHER editable element (a plain
1721
+ // x-edit leaf, an input, someone else's contenteditable): acting now
1722
+ // would restore this area's old saved range and edit text the writer
1723
+ // can see is not selected. The control goes quiet instead. Page
1724
+ // commands write variables, never the selection — they stay live.
1725
+ const live = range();
1726
+ if (!page && live && !area.contains(live.startContainer) && editableElsewhere(live.startContainer)) return false;
1700
1727
  if (cfg.minimal && spec.block && !page) return false;
1701
1728
  if (cfg.mode !== 'html' && !spec.md && !page) return false;
1702
1729
  if (page) return true; // writes to the area, needs no caret
@@ -1710,6 +1737,13 @@
1710
1737
  const spec = COMMANDS[id]; if (!spec) return;
1711
1738
  const page = !!(ctrl && ctrl.page && spec.page);
1712
1739
  if (!page) {
1740
+ // Checked BEFORE restoring the saved range: if the writer's caret is
1741
+ // visibly in another editable element, restoring first would move the
1742
+ // selection back here and the command would edit text the writer can
1743
+ // see is not selected. (The reset blocks pointer clicks on disabled
1744
+ // controls; this covers keyboard activation.)
1745
+ const live = range();
1746
+ if (live && !area.contains(live.startContainer) && editableElsewhere(live.startContainer)) return;
1713
1747
  restoreRange(area);
1714
1748
  // If the caret could not be put in this area — it was never focused, or
1715
1749
  // the saved range has gone stale — do nothing. Running anyway rewrites
@@ -2064,7 +2098,7 @@
2064
2098
  if (a && el.contains(a)) e.preventDefault();
2065
2099
  });
2066
2100
 
2067
- el.addEventListener('focusin', () => { lastFocused = el; sync(); });
2101
+ el.addEventListener('focusin', () => { if (lastFocused && lastFocused !== el) release(lastFocused); lastFocused = el; el.setAttribute('data-text-edit-focused', ''); sync(); });
2068
2102
  // Focus gone somewhere that is neither this area nor one of its controls.
2069
2103
  el.addEventListener('focusout', (e) => {
2070
2104
  if (keeps(el, e.relatedTarget || pointerTarget)) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.220",
3
+ "version": "0.5.222",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",