mnfst 0.5.217 → 0.5.219

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.217';
5
+ const MANIFEST_BUILD_VERSION = '0.5.219';
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,41 @@ 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
+ wireGroup(pre);
639
+ }
640
+
641
+ // Attach behavior to a built group: tab activation, keyboard nav, wrapper copy.
642
+ // Everything is derived from the built DOM, so this wires a group built this
643
+ // session AND one adopted from prerendered output (whose listeners cannot
644
+ // survive serialization). Idempotent via _groupBound.
645
+ function wireGroup(pre) {
646
+ if (pre._groupBound) return;
647
+ pre._groupBound = true;
648
+
649
+ const sourcePanels = Array.from(pre.children).filter(c => c.hasAttribute('name'));
650
+ const ambientCode = Array.from(pre.children).filter(c =>
651
+ !c.hasAttribute('name') && c.tagName === 'PRE' && c.hasAttribute('x-code'));
652
+ const tabButtons = Array.from(pre.querySelectorAll(':scope > header [role="tab"]'));
653
+ const copyBtn = pre.querySelector(':scope > button.copy');
654
+ const wrapperHasCopy = pre.hasAttribute('copy');
655
+ const isHeaderless = !pre.querySelector(':scope > header');
656
+
657
+ let activeName = (tabButtons.find(b => b.getAttribute('aria-selected') === 'true')
658
+ || tabButtons[0] || {}).textContent
659
+ || (sourcePanels[0] && sourcePanels[0].getAttribute('name'))
660
+ || null;
661
+
644
662
  // Visibility toggle via style.display (pre's display:flex outweighs the UA
645
663
  // [hidden] rule). Also flips per-tab copy-button visibility when [copy] is
646
664
  // per-panel rather than on the wrapper.
647
- let activeName = active;
648
665
  function activate(name) {
649
666
  activeName = name;
650
667
  tabButtons.forEach(btn => {
@@ -662,7 +679,47 @@ async function setupCodeGroup(group) {
662
679
  copyBtn.style.display = activeCanCopy ? '' : 'none';
663
680
  }
664
681
  }
665
- if (!isHeaderless) activate(active);
682
+
683
+ tabButtons.forEach(btn => btn.addEventListener('click', () => activate(btn.textContent)));
684
+ tabButtons.forEach((btn, idx) => {
685
+ btn.addEventListener('keydown', (ev) => {
686
+ if (ev.key === 'ArrowRight' || ev.key === 'ArrowLeft') {
687
+ ev.preventDefault();
688
+ const next = ev.key === 'ArrowRight'
689
+ ? (idx + 1) % tabButtons.length
690
+ : (idx - 1 + tabButtons.length) % tabButtons.length;
691
+ tabButtons[next].focus();
692
+ tabButtons[next].click();
693
+ } else if (ev.key === 'Home') {
694
+ ev.preventDefault(); tabButtons[0].focus(); tabButtons[0].click();
695
+ } else if (ev.key === 'End') {
696
+ ev.preventDefault(); tabButtons[tabButtons.length - 1].focus(); tabButtons[tabButtons.length - 1].click();
697
+ }
698
+ });
699
+ });
700
+
701
+ if (copyBtn) {
702
+ copyBtn.addEventListener('click', async () => {
703
+ // Prefer the <pre x-code> panel for copy (its source, not a frame's
704
+ // rendered text). Headerless groups pick the first ambient <pre x-code>.
705
+ let activePanel;
706
+ if (isHeaderless) {
707
+ activePanel = ambientCode[0];
708
+ } else {
709
+ const sameName = sourcePanels.filter(p => p.getAttribute('name') === activeName);
710
+ activePanel = sameName.find(p => p.tagName === 'PRE' && p.hasAttribute('x-code')) || sameName[0];
711
+ }
712
+ if (!activePanel) return;
713
+ const code = activePanel.querySelector(':scope > code') || activePanel;
714
+ try {
715
+ await navigator.clipboard.writeText(code.textContent);
716
+ copyBtn.classList.add('copied');
717
+ setTimeout(() => copyBtn.classList.remove('copied'), 1500);
718
+ } catch { /* clipboard rejected (browser permissions) — fail silently */ }
719
+ });
720
+ }
721
+
722
+ if (!isHeaderless && activeName != null) activate(activeName);
666
723
  }
667
724
 
668
725
  // ─── Page scan + observation ─────────────────────────────────────────────────
@@ -754,10 +811,27 @@ function processOne(el) {
754
811
  }
755
812
  }
756
813
 
814
+ // Prerendered pages ship groups/blocks already BUILT (data-group-processed /
815
+ // data-code-block-built baked in), which the processing pipeline rightly skips —
816
+ // but their listeners cannot survive serialization, so tabs, copy, expand and
817
+ // editors arrive dead. Re-attach behavior to that adopted markup. Cheap (no
818
+ // highlighting), so it runs eagerly rather than through the IntersectionObserver.
819
+ function rebindAdopted(root = document) {
820
+ root.querySelectorAll('[x-code-group][data-group-processed="yes"]').forEach(g => {
821
+ wireGroup(g);
822
+ Array.from(g.children).forEach(c => { if (c.dataset && c.dataset.codeBlockBuilt === 'yes') rebindBlock(c); });
823
+ });
824
+ root.querySelectorAll('pre[data-code-block-built="yes"]').forEach(rebindBlock);
825
+ root.querySelectorAll('code[copy][data-code-processed="yes"]').forEach(el => {
826
+ if (!(el.parentElement && el.parentElement.tagName === 'PRE')) setupInlineCopy(el);
827
+ });
828
+ }
829
+
757
830
  // Observe every unprocessed candidate in `root` (idempotent). Already-visible
758
831
  // elements process immediately so markdown-injected blocks render synchronously
759
832
  // instead of waiting for the next IO callback.
760
833
  function observeAll(root = document) {
834
+ rebindAdopted(root);
761
835
  const io = ensureObserver();
762
836
  const candidates = [
763
837
  ...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,28 @@
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: 0` removes it (e.g. for grid rows where an extra flex
2992
+ item would take a cell). No full-row outline — the grip and the cursor
2993
+ say "draggable" without boxing the content. */
2994
+ :where([data-edit-ui] [data-edit-sortable], [data-edit-ui][data-edit-sortable])::before {
2995
+ content: '';
2996
+ align-self: center;
2997
+ flex: 0 0 auto;
2998
+ width: var(--edit-grip, 0.6rem);
2999
+ height: 1rem;
3000
+ margin-inline-end: calc(var(--edit-grip, 0.6rem) / 2);
3001
+ background-image: radial-gradient(circle, var(--color-content-subtle) 1px, transparent 1px);
3002
+ background-size: 0.3rem 0.3rem;
3003
+ background-position: center;
3004
+ opacity: 0;
3005
+ transition: opacity 0.12s ease;
3006
+ pointer-events: none;
3007
+ }
3008
+
3009
+ :where([data-edit-ui] [data-edit-sortable], [data-edit-ui][data-edit-sortable]):is(:hover, :focus-visible)::before {
3010
+ opacity: 0.9;
2984
3011
  }
2985
3012
 
2986
3013
  :where([data-edit-sortable], [data-edit-movable]):focus-visible {
@@ -3025,25 +3052,30 @@
3025
3052
  touch-action: none;
3026
3053
  }
3027
3054
 
3028
- :where([data-edit-movable]):hover {
3055
+ :where([data-edit-ui] [data-edit-movable], [data-edit-ui][data-edit-movable]):hover {
3029
3056
  outline: 1px solid var(--edit-accent, var(--color-brand-content));
3030
3057
  outline-offset: 2px;
3031
3058
  }
3032
3059
 
3033
- /* ---- Inline text ---- */
3034
- :where([data-edit-armed]) [contenteditable="true"] {
3060
+ /* ---- Inline text ----
3061
+ One weight everywhere: hover hints and the focused caret both use the same
3062
+ 1px line, so focusing doesn't make the page jump a weight class. */
3063
+ :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"],
3064
+ :where([data-edit-ui])[data-edit-armed] [contenteditable="true"] {
3035
3065
  outline: 1px solid transparent;
3036
3066
  outline-offset: 2px;
3037
3067
  border-radius: 3px;
3038
3068
  transition: outline-color 0.12s ease;
3039
3069
  }
3040
3070
 
3041
- :where([data-edit-armed]) [contenteditable="true"]:hover {
3071
+ :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"]:hover,
3072
+ :where([data-edit-ui])[data-edit-armed] [contenteditable="true"]:hover {
3042
3073
  outline-color: color-mix(in oklch, var(--edit-accent, var(--color-brand-content)) 45%, transparent);
3043
3074
  }
3044
3075
 
3045
- :where([data-edit-armed]) [contenteditable="true"]:focus {
3046
- outline: 2px solid var(--edit-accent, var(--color-brand-content));
3076
+ :where([data-edit-ui]) :where([data-edit-armed]) [contenteditable="true"]:focus,
3077
+ :where([data-edit-ui])[data-edit-armed] [contenteditable="true"]:focus {
3078
+ outline-color: var(--edit-accent, var(--color-brand-content));
3047
3079
  }
3048
3080
 
3049
3081
  /* ---- Size handles ----
@@ -3053,14 +3085,16 @@
3053
3085
  position: relative;
3054
3086
  }
3055
3087
 
3056
- /* Handles reveal on approach rather than sitting on the element permanently */
3088
+ /* Handles reveal on approach rather than sitting on the element permanently.
3089
+ The hairline is chrome, so it follows the [data-edit-ui] scope like every
3090
+ other affordance; outside the scope the handles still work (hit areas and
3091
+ resize cursors stay), they just draw nothing. */
3057
3092
  :where([data-edit-sizable]) :where([data-edit-handle])::before {
3058
3093
  opacity: 0;
3059
3094
  transition: opacity 0.12s ease, background-color 0.15s ease;
3060
3095
  }
3061
3096
 
3062
- :where([data-edit-sizable]):hover :where([data-edit-handle])::before,
3063
- :where([data-edit-sizable]):focus-within :where([data-edit-handle])::before {
3097
+ :where([data-edit-ui] [data-edit-sizable], [data-edit-ui][data-edit-sizable]):is(:hover, :focus-within) :where([data-edit-handle])::before {
3064
3098
  opacity: 1;
3065
3099
  }
3066
3100
 
@@ -3220,8 +3254,10 @@
3220
3254
  }
3221
3255
  }
3222
3256
 
3257
+ /* --edit-toolbar: none (any scope up to :root) suppresses it — e.g. a site
3258
+ that demos .authoring regions but has its own publish flow. */
3223
3259
  :root[data-edit-active] :where([data-edit-toolbar]) {
3224
- display: flex;
3260
+ display: var(--edit-toolbar, flex);
3225
3261
  }
3226
3262
 
3227
3263
  /* A popover: the top layer sits above every stacking context, so no handle,
@@ -3918,201 +3954,6 @@
3918
3954
  }
3919
3955
  }
3920
3956
 
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
3957
  /* Manifest Sidebars */
4117
3958
 
4118
3959
  @layer base, components, utilities;
@@ -4450,6 +4291,26 @@
4450
4291
  }
4451
4292
  }
4452
4293
 
4294
+ /* ---- Selection menu (x-text-edit-menu) ----
4295
+ Positioned by the --text-edit-selection-* variables the plugin writes to
4296
+ :root, so the default needs no script; restyle or reposition freely. */
4297
+ :where([data-text-edit-menu]) {
4298
+ position: fixed;
4299
+ margin: 0;
4300
+ inset: auto;
4301
+ left: var(--text-edit-selection-center);
4302
+ top: var(--text-edit-selection-y);
4303
+ transform: translate(-50%, calc(-100% - 0.5rem));
4304
+ display: flex;
4305
+ gap: 0.25rem;
4306
+ align-items: center;
4307
+ padding: 0.25rem;
4308
+ background: var(--color-surface-1);
4309
+ border: 1px solid var(--color-line);
4310
+ border-radius: var(--radius);
4311
+ box-shadow: 0 8px 30px rgb(0 0 0 / 0.2);
4312
+ }
4313
+
4453
4314
  /* ---- Controls ----
4454
4315
  A control is marked wherever it lives, so a toolbar is optional markup and
4455
4316
  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
  }