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.
- package/lib/manifest.appwrite.data.js +1 -1
- package/lib/manifest.code.js +126 -46
- package/lib/manifest.css +94 -222
- package/lib/manifest.d.ts +2 -0
- package/lib/manifest.data.js +26 -3
- package/lib/manifest.defer.js +145 -34
- package/lib/manifest.edit.css +65 -27
- package/lib/manifest.edit.js +213 -28
- package/lib/manifest.integrity.json +11 -11
- package/lib/manifest.js +43 -2
- package/lib/manifest.markdown.js +22 -12
- package/lib/manifest.min.css +1 -1
- package/lib/manifest.router.js +58 -69
- package/lib/manifest.sw.js +1 -1
- package/lib/manifest.text.edit.css +29 -0
- package/lib/manifest.text.edit.js +164 -25
- package/lib/manifest.utilities.js +246 -100
- package/lib/manifest.utilities.node.mjs +195 -35
- package/package.json +1 -1
package/lib/manifest.code.js
CHANGED
|
@@ -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
|
-
|
|
356
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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
|
-
/* ----
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
the region
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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: -
|
|
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-
|
|
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
|
-
:
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|