mnfst 0.5.218 → 0.5.220

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.218';
5
+ const MANIFEST_BUILD_VERSION = '0.5.220';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
@@ -265,6 +265,8 @@ async function setupInline(el, hljs) {
265
265
  }
266
266
 
267
267
  function setupInlineCopy(el) {
268
+ if (el._copyBound) return; // idempotent: fresh setup and the prerender re-bind share it
269
+ el._copyBound = true;
268
270
  if (!el.hasAttribute('role')) el.setAttribute('role', 'button');
269
271
  if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '0');
270
272
  if (!el.hasAttribute('aria-label')) el.setAttribute('aria-label', 'Click to copy');
@@ -350,10 +352,35 @@ async function setupBlock(pre, hljs) {
350
352
  const lang = actualLang || resolveLanguage(hljs, requested);
351
353
  setupEditor(pre, code, lang, hljs);
352
354
  }
355
+
356
+ pre._blockBound = true; // built this session — the prerender re-bind skips it
353
357
  }
354
358
 
355
- function setupBlockCopy(pre, code) {
356
- const btn = document.createElement('button');
359
+ // Re-attach behavior to a block adopted from prerendered output: its copy,
360
+ // expand and editor markup is baked, but listeners cannot survive
361
+ // serialization. Idempotent via _blockBound (set by a fresh setupBlock too).
362
+ function rebindBlock(pre) {
363
+ if (pre._blockBound) return;
364
+ pre._blockBound = true;
365
+ const code = pre.querySelector(':scope > code');
366
+ if (!code) return;
367
+ const copyBtn = pre.querySelector(':scope > button.copy');
368
+ if (copyBtn) setupBlockCopy(pre, code, copyBtn);
369
+ const expandBtn = pre.querySelector(':scope > button.expand');
370
+ if (expandBtn) {
371
+ const collapseAt = parseInt(pre.style.getPropertyValue('--collapse-lines'), 10) || 20;
372
+ const hiddenCount = Math.max(1, code.textContent.split('\n').length - collapseAt);
373
+ wireExpandButton(pre, expandBtn, hiddenCount);
374
+ }
375
+ if (pre.hasAttribute('edit')) {
376
+ const m = (code.className || '').match(/language-([\w-]+)/);
377
+ const lang = (m && m[1]) || pre.getAttribute('x-code') || pre.getAttribute('language') || '';
378
+ loadHighlightJS(lang).then(hljs => setupEditor(pre, code, lang, hljs)).catch(() => { });
379
+ }
380
+ }
381
+
382
+ function setupBlockCopy(pre, code, existingBtn) {
383
+ const btn = existingBtn || document.createElement('button');
357
384
  btn.className = 'copy';
358
385
  btn.type = 'button';
359
386
  btn.setAttribute('aria-label', 'Copy code to clipboard');
@@ -364,7 +391,7 @@ function setupBlockCopy(pre, code) {
364
391
  setTimeout(() => btn.classList.remove('copied'), 1500);
365
392
  } catch { /* clipboard rejected (browser permissions) — fail silently */ }
366
393
  });
367
- pre.appendChild(btn);
394
+ if (!existingBtn) pre.appendChild(btn);
368
395
  }
369
396
 
370
397
  function setupCollapse(pre, code) {
@@ -382,9 +409,13 @@ function setupCollapse(pre, code) {
382
409
  btn.className = 'expand';
383
410
  btn.type = 'button';
384
411
  btn.setAttribute('aria-expanded', 'false');
385
- const hiddenCount = lineCount - collapseAt;
386
- // Visual label is locale-safe ("+N" / "−"); screen readers get an explicit
387
- // English aria-label.
412
+ wireExpandButton(pre, btn, lineCount - collapseAt);
413
+ pre.appendChild(btn);
414
+ }
415
+
416
+ // Visual label is locale-safe ("+N" / "−"); screen readers get an explicit
417
+ // English aria-label. Shared by the build path and the prerender re-bind.
418
+ function wireExpandButton(pre, btn, hiddenCount) {
388
419
  const updateLabel = () => {
389
420
  const collapsed = pre.hasAttribute('data-collapsed');
390
421
  btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
@@ -399,7 +430,6 @@ function setupCollapse(pre, code) {
399
430
  updateLabel();
400
431
  });
401
432
  updateLabel();
402
- pre.appendChild(btn);
403
433
  }
404
434
 
405
435
  async function setupEditor(pre, code, lang, hljs) {
@@ -572,26 +602,9 @@ async function setupCodeGroup(group) {
572
602
  btn.textContent = name;
573
603
  btn.setAttribute('aria-selected', name === active ? 'true' : 'false');
574
604
  btn.tabIndex = name === active ? 0 : -1;
575
- btn.addEventListener('click', () => activate(name));
576
605
  tablist.appendChild(btn);
577
606
  return btn;
578
607
  });
579
- tabButtons.forEach((btn, idx) => {
580
- btn.addEventListener('keydown', (ev) => {
581
- if (ev.key === 'ArrowRight' || ev.key === 'ArrowLeft') {
582
- ev.preventDefault();
583
- const next = ev.key === 'ArrowRight'
584
- ? (idx + 1) % tabButtons.length
585
- : (idx - 1 + tabButtons.length) % tabButtons.length;
586
- tabButtons[next].focus();
587
- tabButtons[next].click();
588
- } else if (ev.key === 'Home') {
589
- ev.preventDefault(); tabButtons[0].focus(); tabButtons[0].click();
590
- } else if (ev.key === 'End') {
591
- ev.preventDefault(); tabButtons[tabButtons.length - 1].focus(); tabButtons[tabButtons.length - 1].click();
592
- }
593
- });
594
- });
595
608
  }
596
609
  if (header) pre.insertBefore(header, pre.firstChild);
597
610
 
@@ -614,37 +627,47 @@ async function setupCodeGroup(group) {
614
627
  // top-end without competing with the header's scroll region.
615
628
  const wrapperHasCopy = pre.hasAttribute('copy');
616
629
  const anyPanelCopy = [...sourcePanels, ...ambientChildren].some(p => p.hasAttribute('copy'));
617
- let copyBtn = null;
618
630
  if (wrapperHasCopy || anyPanelCopy) {
619
- copyBtn = document.createElement('button');
631
+ const copyBtn = document.createElement('button');
620
632
  copyBtn.className = 'copy';
621
633
  copyBtn.type = 'button';
622
634
  copyBtn.setAttribute('aria-label', 'Copy code to clipboard');
623
- copyBtn.addEventListener('click', async () => {
624
- // Prefer the <pre x-code> panel for copy (its source, not a frame's
625
- // rendered text). Headerless groups pick the first ambient <pre x-code>.
626
- let activePanel;
627
- if (isHeaderless) {
628
- activePanel = ambientChildren.find(p => p.tagName === 'PRE' && p.hasAttribute('x-code'));
629
- } else {
630
- const sameName = sourcePanels.filter(p => p.getAttribute('name') === activeName);
631
- activePanel = sameName.find(p => p.tagName === 'PRE' && p.hasAttribute('x-code')) || sameName[0];
632
- }
633
- if (!activePanel) return;
634
- const code = activePanel.querySelector(':scope > code') || activePanel;
635
- try {
636
- await navigator.clipboard.writeText(code.textContent);
637
- copyBtn.classList.add('copied');
638
- setTimeout(() => copyBtn.classList.remove('copied'), 1500);
639
- } catch { /* clipboard rejected (browser permissions) — fail silently */ }
640
- });
641
635
  pre.appendChild(copyBtn);
642
636
  }
643
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';
644
+ wireGroup(pre);
645
+ }
646
+
647
+ // Attach behavior to a built group: tab activation, keyboard nav, wrapper copy.
648
+ // Everything is derived from the built DOM, so this wires a group built this
649
+ // session AND one adopted from prerendered output (whose listeners cannot
650
+ // survive serialization). Idempotent via _groupBound.
651
+ function wireGroup(pre) {
652
+ if (pre._groupBound) return;
653
+ pre._groupBound = true;
654
+
655
+ const sourcePanels = Array.from(pre.children).filter(c => c.hasAttribute('name'));
656
+ const ambientCode = Array.from(pre.children).filter(c =>
657
+ !c.hasAttribute('name') && c.tagName === 'PRE' && c.hasAttribute('x-code'));
658
+ const tabButtons = Array.from(pre.querySelectorAll(':scope > header [role="tab"]'));
659
+ const copyBtn = pre.querySelector(':scope > button.copy');
660
+ const wrapperHasCopy = pre.hasAttribute('copy');
661
+ const isHeaderless = !pre.querySelector(':scope > header');
662
+
663
+ let activeName = (tabButtons.find(b => b.getAttribute('aria-selected') === 'true')
664
+ || tabButtons[0] || {}).textContent
665
+ || (sourcePanels[0] && sourcePanels[0].getAttribute('name'))
666
+ || null;
667
+
644
668
  // Visibility toggle via style.display (pre's display:flex outweighs the UA
645
669
  // [hidden] rule). Also flips per-tab copy-button visibility when [copy] is
646
670
  // per-panel rather than on the wrapper.
647
- let activeName = active;
648
671
  function activate(name) {
649
672
  activeName = name;
650
673
  tabButtons.forEach(btn => {
@@ -662,7 +685,47 @@ async function setupCodeGroup(group) {
662
685
  copyBtn.style.display = activeCanCopy ? '' : 'none';
663
686
  }
664
687
  }
665
- if (!isHeaderless) activate(active);
688
+
689
+ tabButtons.forEach(btn => btn.addEventListener('click', () => activate(btn.textContent)));
690
+ tabButtons.forEach((btn, idx) => {
691
+ btn.addEventListener('keydown', (ev) => {
692
+ if (ev.key === 'ArrowRight' || ev.key === 'ArrowLeft') {
693
+ ev.preventDefault();
694
+ const next = ev.key === 'ArrowRight'
695
+ ? (idx + 1) % tabButtons.length
696
+ : (idx - 1 + tabButtons.length) % tabButtons.length;
697
+ tabButtons[next].focus();
698
+ tabButtons[next].click();
699
+ } else if (ev.key === 'Home') {
700
+ ev.preventDefault(); tabButtons[0].focus(); tabButtons[0].click();
701
+ } else if (ev.key === 'End') {
702
+ ev.preventDefault(); tabButtons[tabButtons.length - 1].focus(); tabButtons[tabButtons.length - 1].click();
703
+ }
704
+ });
705
+ });
706
+
707
+ if (copyBtn) {
708
+ copyBtn.addEventListener('click', async () => {
709
+ // Prefer the <pre x-code> panel for copy (its source, not a frame's
710
+ // rendered text). Headerless groups pick the first ambient <pre x-code>.
711
+ let activePanel;
712
+ if (isHeaderless) {
713
+ activePanel = ambientCode[0];
714
+ } else {
715
+ const sameName = sourcePanels.filter(p => p.getAttribute('name') === activeName);
716
+ activePanel = sameName.find(p => p.tagName === 'PRE' && p.hasAttribute('x-code')) || sameName[0];
717
+ }
718
+ if (!activePanel) return;
719
+ const code = activePanel.querySelector(':scope > code') || activePanel;
720
+ try {
721
+ await navigator.clipboard.writeText(code.textContent);
722
+ copyBtn.classList.add('copied');
723
+ setTimeout(() => copyBtn.classList.remove('copied'), 1500);
724
+ } catch { /* clipboard rejected (browser permissions) — fail silently */ }
725
+ });
726
+ }
727
+
728
+ if (!isHeaderless && activeName != null) activate(activeName);
666
729
  }
667
730
 
668
731
  // ─── Page scan + observation ─────────────────────────────────────────────────
@@ -754,10 +817,27 @@ function processOne(el) {
754
817
  }
755
818
  }
756
819
 
820
+ // Prerendered pages ship groups/blocks already BUILT (data-group-processed /
821
+ // data-code-block-built baked in), which the processing pipeline rightly skips —
822
+ // but their listeners cannot survive serialization, so tabs, copy, expand and
823
+ // editors arrive dead. Re-attach behavior to that adopted markup. Cheap (no
824
+ // highlighting), so it runs eagerly rather than through the IntersectionObserver.
825
+ function rebindAdopted(root = document) {
826
+ root.querySelectorAll('[x-code-group][data-group-built="yes"]').forEach(g => {
827
+ wireGroup(g);
828
+ Array.from(g.children).forEach(c => { if (c.dataset && c.dataset.codeBlockBuilt === 'yes') rebindBlock(c); });
829
+ });
830
+ root.querySelectorAll('pre[data-code-block-built="yes"]').forEach(rebindBlock);
831
+ root.querySelectorAll('code[copy][data-code-processed="yes"]').forEach(el => {
832
+ if (!(el.parentElement && el.parentElement.tagName === 'PRE')) setupInlineCopy(el);
833
+ });
834
+ }
835
+
757
836
  // Observe every unprocessed candidate in `root` (idempotent). Already-visible
758
837
  // elements process immediately so markdown-injected blocks render synchronously
759
838
  // instead of waiting for the next IO callback.
760
839
  function observeAll(root = document) {
840
+ rebindAdopted(root);
761
841
  const io = ensureObserver();
762
842
  const candidates = [
763
843
  ...root.querySelectorAll('[x-code]:not([data-code-processed])'),
package/lib/manifest.css CHANGED
@@ -2929,34 +2929,44 @@
2929
2929
 
2930
2930
  @layer utilities {
2931
2931
 
2932
- /* ---- Editable area (armed) ----
2933
- Chrome is opt-in. Most uses of this plugin are a resize handle or a sortable
2934
- list, which should look like themselves; only .authoring gets the outline and
2935
- the region label. */
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. */
2939
+
2940
+ /* ---- Editable area (armed) ---- */
2936
2941
  :where([data-edit-armed]) {
2937
2942
  position: relative;
2938
2943
  }
2939
2944
 
2940
2945
  /* Quiet until touched. A page covered in dashed boxes reads as a diagram of
2941
- itself; the affordance only has to be there when you go looking for it. */
2942
- :where([data-edit-armed][data-edit-authoring]) {
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] *) {
2943
2950
  outline: 1px dashed transparent;
2944
2951
  outline-offset: 6px;
2945
2952
  border-radius: var(--radius);
2946
2953
  transition: outline-color 0.12s ease;
2947
2954
  }
2948
2955
 
2949
- :where([data-edit-armed][data-edit-authoring]):hover,
2950
- :where([data-edit-armed][data-edit-authoring]):focus-within {
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))) {
2951
2957
  outline-color: var(--color-line);
2952
2958
  }
2953
2959
 
2954
- :where([data-edit-armed][data-edit-authoring])::before {
2960
+ :where([data-edit-armed]):where([data-edit-ui], [data-edit-ui] *)::before {
2955
2961
  content: attr(data-edit-label);
2956
2962
  position: absolute;
2957
- inset-block-start: -1.5rem;
2958
- inset-inline-start: 0;
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;
2959
2968
  font-size: 0.7rem;
2969
+ line-height: 1.2;
2960
2970
  letter-spacing: 0.04em;
2961
2971
  text-transform: uppercase;
2962
2972
  color: var(--color-content-subtle);
@@ -2965,8 +2975,7 @@
2965
2975
  transition: opacity 0.12s ease;
2966
2976
  }
2967
2977
 
2968
- :where([data-edit-armed][data-edit-authoring]):hover::before,
2969
- :where([data-edit-armed][data-edit-authoring]):focus-within::before {
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 {
2970
2979
  opacity: 1;
2971
2980
  }
2972
2981
 
@@ -2977,10 +2986,30 @@
2977
2986
  transition: opacity 0.12s ease, outline-color 0.12s ease;
2978
2987
  }
2979
2988
 
2980
- :where([data-edit-sortable]):hover,
2981
- :where([data-edit-sortable]):focus-within {
2982
- outline: 1px solid var(--edit-accent, var(--color-brand-content));
2983
- outline-offset: 3px;
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;
2984
3013
  }
2985
3014
 
2986
3015
  :where([data-edit-sortable], [data-edit-movable]):focus-visible {
@@ -3025,25 +3054,30 @@
3025
3054
  touch-action: none;
3026
3055
  }
3027
3056
 
3028
- :where([data-edit-movable]):hover {
3057
+ :where([data-edit-ui] [data-edit-movable], [data-edit-ui][data-edit-movable]):hover {
3029
3058
  outline: 1px solid var(--edit-accent, var(--color-brand-content));
3030
3059
  outline-offset: 2px;
3031
3060
  }
3032
3061
 
3033
- /* ---- Inline text ---- */
3034
- :where([data-edit-armed]) [contenteditable="true"] {
3062
+ /* ---- Inline text ----
3063
+ One weight everywhere: hover hints and the focused caret both use the same
3064
+ 1px line, so focusing doesn't make the page jump a weight class. */
3065
+ :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"],
3066
+ :where([data-edit-ui])[data-edit-armed] [contenteditable="true"] {
3035
3067
  outline: 1px solid transparent;
3036
3068
  outline-offset: 2px;
3037
3069
  border-radius: 3px;
3038
3070
  transition: outline-color 0.12s ease;
3039
3071
  }
3040
3072
 
3041
- :where([data-edit-armed]) [contenteditable="true"]:hover {
3073
+ :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"]:hover,
3074
+ :where([data-edit-ui])[data-edit-armed] [contenteditable="true"]:hover {
3042
3075
  outline-color: color-mix(in oklch, var(--edit-accent, var(--color-brand-content)) 45%, transparent);
3043
3076
  }
3044
3077
 
3045
- :where([data-edit-armed]) [contenteditable="true"]:focus {
3046
- outline: 2px solid var(--edit-accent, var(--color-brand-content));
3078
+ :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"]:focus,
3079
+ :where([data-edit-ui])[data-edit-armed] [contenteditable="true"]:focus {
3080
+ outline-color: var(--edit-accent, var(--color-brand-content));
3047
3081
  }
3048
3082
 
3049
3083
  /* ---- Size handles ----
@@ -3053,14 +3087,16 @@
3053
3087
  position: relative;
3054
3088
  }
3055
3089
 
3056
- /* Handles reveal on approach rather than sitting on the element permanently */
3090
+ /* Handles reveal on approach rather than sitting on the element permanently.
3091
+ The hairline is chrome, so it follows the [data-edit-ui] scope like every
3092
+ other affordance; outside the scope the handles still work (hit areas and
3093
+ resize cursors stay), they just draw nothing. */
3057
3094
  :where([data-edit-sizable]) :where([data-edit-handle])::before {
3058
3095
  opacity: 0;
3059
3096
  transition: opacity 0.12s ease, background-color 0.15s ease;
3060
3097
  }
3061
3098
 
3062
- :where([data-edit-sizable]):hover :where([data-edit-handle])::before,
3063
- :where([data-edit-sizable]):focus-within :where([data-edit-handle])::before {
3099
+ :where([data-edit-ui] [data-edit-sizable], [data-edit-ui][data-edit-sizable]):is(:hover, :focus-within) :where([data-edit-handle])::before {
3064
3100
  opacity: 1;
3065
3101
  }
3066
3102
 
@@ -3220,8 +3256,10 @@
3220
3256
  }
3221
3257
  }
3222
3258
 
3259
+ /* --edit-toolbar: none (any scope up to :root) suppresses it — e.g. a site
3260
+ that demos .authoring regions but has its own publish flow. */
3223
3261
  :root[data-edit-active] :where([data-edit-toolbar]) {
3224
- display: flex;
3262
+ display: var(--edit-toolbar, flex);
3225
3263
  }
3226
3264
 
3227
3265
  /* A popover: the top layer sits above every stacking context, so no handle,
@@ -3918,201 +3956,6 @@
3918
3956
  }
3919
3957
  }
3920
3958
 
3921
- /* Manifest Resize Handles
3922
-
3923
- Superseded by x-edit.size (manifest.edit.css) — kept for existing projects.
3924
- Self-contained: handle geometry lives here, not in a shared snippet. */
3925
-
3926
- @layer base, components, utilities;
3927
-
3928
- @layer utilities {
3929
-
3930
- :where([x-resize]) {
3931
- position: relative;
3932
-
3933
- /* Resize handle area */
3934
- .resize-handle {
3935
- position: absolute;
3936
- display: block;
3937
- width: var(--h);
3938
- height: var(--h);
3939
- z-index: 100;
3940
- background: transparent;
3941
- border: none;
3942
- outline: none;
3943
- --h: var(--spacing-resize-handle, 1rem);
3944
-
3945
- /* Visible handle */
3946
- &::before {
3947
- content: '';
3948
- position: absolute;
3949
- top: 50%;
3950
- left: 50%;
3951
- transform: translate(-50%, -50%);
3952
- width: 1px;
3953
- height: 1px;
3954
- background: transparent;
3955
- transition: background-color 0.2s ease;
3956
- pointer-events: none
3957
- }
3958
-
3959
- &:hover::before,
3960
- &:active::before {
3961
- background-color: var(--color-line, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent));
3962
- }
3963
- }
3964
-
3965
- /* Handle geometry — edges span their axis, corners stay square */
3966
- .resize-handle-top,
3967
- .resize-handle-bottom {
3968
- left: 0;
3969
- width: 100%;
3970
- height: var(--h);
3971
- cursor: ns-resize;
3972
-
3973
- &::before {
3974
- width: 100%
3975
- }
3976
- }
3977
-
3978
- .resize-handle-left,
3979
- .resize-handle-right,
3980
- .resize-handle-start,
3981
- .resize-handle-end {
3982
- top: 0;
3983
- width: var(--h);
3984
- height: 100%;
3985
- cursor: ew-resize;
3986
-
3987
- &::before {
3988
- height: 100%
3989
- }
3990
- }
3991
-
3992
- .resize-handle-top {
3993
- top: calc(var(--h) * -0.5)
3994
- }
3995
-
3996
- .resize-handle-bottom {
3997
- bottom: calc(var(--h) * -0.5)
3998
- }
3999
-
4000
- .resize-handle-left,
4001
- .resize-handle-top-left,
4002
- .resize-handle-bottom-left {
4003
- left: calc(var(--h) * -0.5)
4004
- }
4005
-
4006
- .resize-handle-right,
4007
- .resize-handle-top-right,
4008
- .resize-handle-bottom-right {
4009
- right: calc(var(--h) * -0.5)
4010
- }
4011
-
4012
- /* Logical edges — inset-inline-* auto-flips for RTL */
4013
- .resize-handle-start,
4014
- .resize-handle-top-start,
4015
- .resize-handle-bottom-start {
4016
- inset-inline-start: calc(var(--h) * -0.5)
4017
- }
4018
-
4019
- .resize-handle-end,
4020
- .resize-handle-top-end,
4021
- .resize-handle-bottom-end {
4022
- inset-inline-end: calc(var(--h) * -0.5)
4023
- }
4024
-
4025
- .resize-handle-top-left,
4026
- .resize-handle-top-right,
4027
- .resize-handle-top-start,
4028
- .resize-handle-top-end {
4029
- top: calc(var(--h) * -0.5)
4030
- }
4031
-
4032
- .resize-handle-bottom-left,
4033
- .resize-handle-bottom-right,
4034
- .resize-handle-bottom-start,
4035
- .resize-handle-bottom-end {
4036
- bottom: calc(var(--h) * -0.5)
4037
- }
4038
-
4039
- /* Corner cursors */
4040
- .resize-handle-top-left,
4041
- .resize-handle-top-start {
4042
- cursor: nw-resize;
4043
- }
4044
-
4045
- .resize-handle-top-right,
4046
- .resize-handle-top-end {
4047
- cursor: ne-resize;
4048
- }
4049
-
4050
- .resize-handle-bottom-left,
4051
- .resize-handle-bottom-start {
4052
- cursor: sw-resize;
4053
- }
4054
-
4055
- .resize-handle-bottom-right,
4056
- .resize-handle-bottom-end {
4057
- cursor: se-resize;
4058
- }
4059
-
4060
- /* Screen overlay to get mouse coordinates */
4061
- .resize-overlay {
4062
- position: fixed;
4063
- top: 0;
4064
- left: 0;
4065
- width: 100vw;
4066
- height: 100vh;
4067
- z-index: 9999;
4068
- display: none;
4069
- background: transparent
4070
- }
4071
-
4072
- /* Resize states */
4073
- .resizable-closing {
4074
- opacity: 0.5;
4075
- transition: opacity 0.2s ease
4076
- }
4077
-
4078
- .resizable-closed {
4079
- display: none !important
4080
- }
4081
-
4082
- /* Focus states for accessibility — framework ring (manifest.theme.css) */
4083
- .resize-handle:focus {
4084
- outline: none;
4085
- box-shadow: 0 0 0 2px color-mix(in oklch, var(--color-content-stark) 30%, transparent)
4086
- }
4087
-
4088
- /* Reduced motion support */
4089
- @media (prefers-reduced-motion: reduce) {
4090
-
4091
- :where(.resizable-closing,
4092
- .resize-handle) {
4093
- transition: none
4094
- }
4095
- }
4096
- }
4097
-
4098
- /* RTL cursor flips — placement follows the logical insets in the shared geometry */
4099
- [dir=rtl] :where([x-resize]) .resize-handle-top-start {
4100
- cursor: ne-resize;
4101
- }
4102
-
4103
- [dir=rtl] :where([x-resize]) .resize-handle-top-end {
4104
- cursor: nw-resize;
4105
- }
4106
-
4107
- [dir=rtl] :where([x-resize]) .resize-handle-bottom-start {
4108
- cursor: se-resize;
4109
- }
4110
-
4111
- [dir=rtl] :where([x-resize]) .resize-handle-bottom-end {
4112
- cursor: sw-resize;
4113
- }
4114
- }
4115
-
4116
3959
  /* Manifest Sidebars */
4117
3960
 
4118
3961
  @layer base, components, utilities;
@@ -4450,6 +4293,35 @@
4450
4293
  }
4451
4294
  }
4452
4295
 
4296
+ /* ---- Selection menu (x-text-edit-menu) ----
4297
+ Positioned by the --text-edit-selection-* variables the plugin writes to
4298
+ :root, so the default needs no script; restyle or reposition freely. */
4299
+ :where([data-text-edit-menu]) {
4300
+ position: fixed;
4301
+ margin: 0;
4302
+ inset: auto;
4303
+ left: var(--text-edit-selection-center);
4304
+ top: var(--text-edit-selection-y);
4305
+ transform: translate(-50%, calc(-100% - 0.5rem));
4306
+ display: flex;
4307
+ gap: 0.25rem;
4308
+ align-items: center;
4309
+ padding: 0.25rem;
4310
+ background: var(--color-surface-1);
4311
+ border: 1px solid var(--color-line);
4312
+ border-radius: var(--radius);
4313
+ box-shadow: 0 8px 30px rgb(0 0 0 / 0.2);
4314
+ }
4315
+
4316
+ /* Without the Popover API the attribute is inert and the menu would render
4317
+ permanently fixed on the page — hide it there; the bubble is progressive
4318
+ enhancement. (In supporting browsers a closed popover is hidden natively.) */
4319
+ @supports not selector(:popover-open) {
4320
+ :where([data-text-edit-menu]) {
4321
+ display: none;
4322
+ }
4323
+ }
4324
+
4453
4325
  /* ---- Controls ----
4454
4326
  A control is marked wherever it lives, so a toolbar is optional markup and
4455
4327
  never a requirement. The reset already dims [aria-disabled=true]. */
package/lib/manifest.d.ts CHANGED
@@ -384,6 +384,8 @@ export interface ManifestEdit {
384
384
  paste(el?: Element): boolean;
385
385
  duplicate(el?: Element): boolean;
386
386
  remove(el?: Element): boolean;
387
+ /** Move a block into another static region (area element or x-edit key), before `ref` or at the end. */
388
+ move(el: Element | null, dest: Element | string, before?: Element | null): boolean;
387
389
  /** The addressable block containing a node, or null. */
388
390
  block(node: Element): Element | null;
389
391
  }