@panphora/bevel 0.2.0 → 0.3.0

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.
@@ -6,12 +6,15 @@ import { type ChangeDetails } from '../internal/controllable-state.js';
6
6
  import { Button } from './button.js';
7
7
  export declare const wrapCompartment: Compartment;
8
8
  export declare const darkCompartment: Compartment;
9
+ export declare const readOnlyCompartment: Compartment;
9
10
  /** iOS zooms the page when a focused editable is under 16px, contenteditable included. */
10
11
  export declare function coarsePointer(): boolean;
11
12
  /** Wrapping is remembered per device. A blocked or absent store falls back to the pointer. */
12
13
  export declare function storedWrap(coarse: boolean): boolean;
13
14
  export declare function rememberWrap(on: boolean): boolean;
14
15
  export declare const wrapExtension: (on: boolean) => Extension;
16
+ /** Read-only still takes focus, so the keyboard can move, select and search; only edits are refused. */
17
+ export declare const readOnlyExtension: (on: boolean) => Extension;
15
18
  export declare const bevelHighlight: HighlightStyle;
16
19
  /** One theme serves both modes: the tokens are light-dark() and the dark flag is a facet. */
17
20
  export declare function bevelEditorTheme(coarse: boolean): Extension;
@@ -22,8 +25,10 @@ export interface CodeEditorExtensionOptions {
22
25
  onSave: () => void;
23
26
  onDocChange: () => void;
24
27
  onFindChange: (open: boolean) => void;
28
+ readOnly: boolean;
29
+ onFind: () => void;
25
30
  }
26
- export declare function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFindChange }: CodeEditorExtensionOptions): Extension[];
31
+ export declare function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFindChange, readOnly, onFind }: CodeEditorExtensionOptions): Extension[];
27
32
  export type CodeEditorAction = 'undo' | 'redo' | 'find' | 'outdent' | 'indent' | 'wrap' | 'fold' | 'suggest';
28
33
  interface CodeEditorState {
29
34
  view: EditorView | null;
@@ -36,6 +41,8 @@ interface CodeEditorState {
36
41
  /** The host clears the flag through this once its own write has landed. `save()` never clears it. */
37
42
  setDirty: (next: boolean | ((previous: boolean) => boolean), details: ChangeDetails) => void;
38
43
  findOpen: boolean;
44
+ findRequest: number;
45
+ readOnly: boolean;
39
46
  extensions: Extension[];
40
47
  root: RefObject<HTMLDivElement | null>;
41
48
  save: () => void;
@@ -52,11 +59,13 @@ export interface CodeEditorProps extends Omit<ComponentProps<'div'>, 'onChange'>
52
59
  wrap?: boolean;
53
60
  defaultWrap?: boolean;
54
61
  onWrapChange?: (wrap: boolean, details: ChangeDetails) => void;
62
+ /** Keeps the editor focusable and selectable, and refuses edits. */
63
+ readOnly?: boolean;
55
64
  /** The mounted view, and null when it is torn down. The tab drives live sync through it. */
56
65
  onViewChange?: (view: EditorView | null) => void;
57
66
  extensions?: Extension[];
58
67
  }
59
- declare function Root({ doc, onSave, dirty, defaultDirty, onDirtyChange, wrap, defaultWrap, onWrapChange, onViewChange, extensions, ref, children, ...props }: CodeEditorProps): import("react").JSX.Element;
68
+ declare function Root({ doc, onSave, dirty, defaultDirty, onDirtyChange, wrap, defaultWrap, onWrapChange, readOnly, onViewChange, extensions, ref, onKeyDown, children, ...props }: CodeEditorProps): import("react").JSX.Element;
60
69
  declare function Dirty({ children, hidden, ...props }: ComponentProps<'i'>): import("react").JSX.Element;
61
70
  export interface CodeEditorToolProps extends ComponentProps<'button'> {
62
71
  action?: CodeEditorAction;
@@ -64,6 +64,7 @@ const WRAP_STORAGE_KEY = "bevel.code-editor.wrap";
64
64
  const FONT = "var(--bevel-font-mono)";
65
65
  const wrapCompartment = new Compartment();
66
66
  const darkCompartment = new Compartment();
67
+ const readOnlyCompartment = new Compartment();
67
68
  function coarsePointer() {
68
69
  if (typeof window === "undefined") return false;
69
70
  return window.matchMedia("(pointer: coarse)").matches || navigator.maxTouchPoints > 0;
@@ -122,6 +123,7 @@ const hangingIndent = ViewPlugin.fromClass(class {
122
123
  }
123
124
  }, { decorations: (plugin) => plugin.decorations });
124
125
  const wrapExtension = (on) => on ? [EditorView.lineWrapping, hangingIndent] : [];
126
+ const readOnlyExtension = (on) => on ? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ inputmode: "none" })] : [];
125
127
  const bevelHighlight = HighlightStyle.define([
126
128
  { tag: [t.comment, t.lineComment, t.blockComment, t.docComment], color: "var(--bevel-syn-comment)", fontStyle: "italic" },
127
129
  { tag: [t.tagName, t.standard(t.tagName)], color: "var(--bevel-syn-tag)", fontWeight: "500" },
@@ -382,7 +384,7 @@ function findPanel(coarse) {
382
384
  };
383
385
  };
384
386
  }
385
- function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFindChange }) {
387
+ function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFindChange, readOnly, onFind }) {
386
388
  return [
387
389
  lineNumbers(),
388
390
  highlightActiveLineGutter(),
@@ -415,6 +417,7 @@ function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFi
415
417
  ...coarse ? [] : [rectangularSelection(), crosshairCursor()],
416
418
  wrapCompartment.of(wrapExtension(wrapped)),
417
419
  darkCompartment.of(EditorView.darkTheme.of(dark)),
420
+ readOnlyCompartment.of(readOnlyExtension(readOnly)),
418
421
  bevelEditorTheme(coarse),
419
422
  EditorView.contentAttributes.of({
420
423
  autocorrect: "off",
@@ -424,6 +427,7 @@ function codeEditorExtensions({ coarse, wrapped, dark, onSave, onDocChange, onFi
424
427
  "aria-label": "Source"
425
428
  }),
426
429
  Prec.highest(keymap.of([
430
+ { key: "Mod-f", preventDefault: true, run: () => (onFind(), true) },
427
431
  { key: "Mod-s", preventDefault: true, run: () => (onSave(), true) },
428
432
  { key: "Mod-Shift-s", preventDefault: true, run: () => (onSave(), true) },
429
433
  { key: "Mod-]", preventDefault: true, run: indentMore },
@@ -502,7 +506,7 @@ const StatusPart = nativePart("div", "bevel-r-code-editor__status");
502
506
  const MetaPart = nativePart("span", "bevel-r-code-editor__meta");
503
507
  const BarPart = nativePart("div", "bevel-r-code-editor__bar");
504
508
  const BarToolsPart = nativePart("div", "bevel-r-code-editor__bar-tools");
505
- function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, defaultWrap, onWrapChange, onViewChange, extensions, ref, children, ...props }) {
509
+ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, defaultWrap, onWrapChange, readOnly = false, onViewChange, extensions, ref, onKeyDown, children, ...props }) {
506
510
  const [view, setView] = useState(null);
507
511
  const viewRef = useRef(null);
508
512
  const root = useRef(null);
@@ -512,6 +516,7 @@ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, d
512
516
  const [isDirty, setDirty] = useControllableState({ value: dirty, defaultValue: defaultDirty, onChange: onDirtyChange });
513
517
  const [wrapped, setWrap] = useControllableState({ value: wrap, defaultValue: initialWrap, onChange: onWrapChange });
514
518
  const [findOpen, setFindOpen] = useState(false);
519
+ const [findRequest, setFindRequest] = useState(0);
515
520
  const { mode } = useBevel();
516
521
  const dark = useResolvedDark(mode);
517
522
  const latest = useRef({ onSave, onViewChange, wrapped });
@@ -527,6 +532,12 @@ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, d
527
532
  setView(next);
528
533
  latest.current.onViewChange?.(next);
529
534
  }, []);
535
+ const requestFind = useCallback(() => {
536
+ const current = viewRef.current;
537
+ if (!current) return;
538
+ openSearchPanel(current);
539
+ setFindRequest((count) => count + 1);
540
+ }, []);
530
541
  const run = useCallback((action) => {
531
542
  const current = viewRef.current;
532
543
  if (!current) return;
@@ -540,20 +551,22 @@ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, d
540
551
  if (searchPanelOpen(current.state)) {
541
552
  closeSearchPanel(current);
542
553
  current.focus();
543
- } else openSearchPanel(current);
554
+ } else requestFind();
544
555
  return;
545
556
  } else if (action === "wrap") {
546
557
  setWrap(!latest.current.wrapped, { reason: "toolbar" });
547
558
  }
548
559
  current.focus();
549
- }, [setWrap]);
560
+ }, [setWrap, requestFind]);
550
561
  const mounted = useMemo(() => codeEditorExtensions({
551
562
  coarse,
552
563
  wrapped: initialWrap,
553
564
  dark,
565
+ readOnly,
554
566
  onSave: save,
555
567
  onDocChange: () => setDirty(true, { reason: "edit" }),
556
- onFindChange: setFindOpen
568
+ onFindChange: setFindOpen,
569
+ onFind: requestFind
557
570
  }), []);
558
571
  useEffect(() => {
559
572
  view?.dispatch({ effects: wrapCompartment.reconfigure(wrapExtension(wrapped)) });
@@ -561,6 +574,9 @@ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, d
561
574
  useEffect(() => {
562
575
  view?.dispatch({ effects: darkCompartment.reconfigure(EditorView.darkTheme.of(dark)) });
563
576
  }, [view, dark]);
577
+ useEffect(() => {
578
+ view?.dispatch({ effects: readOnlyCompartment.reconfigure(readOnlyExtension(readOnly)) });
579
+ }, [view, readOnly]);
564
580
  const settled = useRef(false);
565
581
  useEffect(() => {
566
582
  if (!settled.current) {
@@ -579,12 +595,20 @@ function Root({ doc, onSave, dirty, defaultDirty = false, onDirtyChange, wrap, d
579
595
  dirty: isDirty,
580
596
  setDirty,
581
597
  findOpen,
598
+ findRequest,
599
+ readOnly,
582
600
  extensions: extensions ? [...mounted, ...extensions] : mounted,
583
601
  root,
584
602
  save,
585
603
  run
586
- }), [view, register, doc, coarse, dark, wrapped, isDirty, setDirty, findOpen, extensions, mounted, save, run]);
587
- return /* @__PURE__ */ jsx(CodeEditorContext, { value: context, children: /* @__PURE__ */ jsx(RootPart, { ...props, ref: merged, children }) });
604
+ }), [view, register, doc, coarse, dark, wrapped, isDirty, setDirty, findOpen, findRequest, readOnly, extensions, mounted, save, run]);
605
+ return /* @__PURE__ */ jsx(CodeEditorContext, { value: context, children: /* @__PURE__ */ jsx(RootPart, { ...props, ref: merged, onKeyDown: (event) => {
606
+ onKeyDown?.(event);
607
+ if (event.defaultPrevented || event.altKey || event.shiftKey || !(event.metaKey || event.ctrlKey)) return;
608
+ if (event.key.toLowerCase() !== "f") return;
609
+ event.preventDefault();
610
+ requestFind();
611
+ }, children }) });
588
612
  }
589
613
  function Dirty({ children, hidden, ...props }) {
590
614
  const { dirty } = useCodeEditor();
@@ -687,6 +711,8 @@ export {
687
711
  coarsePointer,
688
712
  codeEditorExtensions,
689
713
  darkCompartment,
714
+ readOnlyCompartment,
715
+ readOnlyExtension,
690
716
  rememberWrap,
691
717
  storedWrap,
692
718
  useCodeEditor,
@@ -7,7 +7,7 @@ const distributions = {
7
7
  richclay: ["richclay.js", "richclay.css", "LICENSE", "THIRD-PARTY-NOTICES.md", "vendor/squire.js", "vendor/squire.provenance.json", "vendor/SQUIRE-LICENSE", "vendor/purify.min.js", "vendor/DOMPURIFY-LICENSE", "vendor/DOMPURIFY-LICENSE-MPL"],
8
8
  quickcrop: ["quickcrop.js", "quickcrop.css", "LICENSE"],
9
9
  hypercms: ["dist/hypercms.min.js", "LICENSE"],
10
- "clay-mutation": ["src/lib/mutation.js", "src/lib/extension-noise.js", "src/lib/region-policy.js", "src/lib/region-capabilities.js", "src/lib/root-attrs.js", "src/lib/user-gesture.js", "LICENSE"]
10
+ "clay-mutation": ["src/lib/mutation.js", "src/lib/extension-noise.js", "src/lib/region-policy.js", "src/lib/region-capabilities.js", "src/lib/root-attrs.js", "src/lib/user-gesture.js", "src/lib/ephemeral.js", "src/lib/derived.js", "src/vendor/sap-ownership.vendor.js", "src/core/boot-phase.js", "LICENSE"]
11
11
  };
12
12
  const packageNames = { hypercms: "@panphora/hyper-cms", "clay-mutation": "@panphora/clayjs" };
13
13
  const [name, source] = process.argv.slice(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panphora/bevel",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Beveled controls in an editorial page. One stylesheet, one script, no build.",
5
5
  "license": "MIT-0",
6
6
  "author": "David Miranda",
package/react/README.md CHANGED
@@ -225,7 +225,7 @@ Each row links to a real example compiled against the installed package. Each co
225
225
  | persistent-action-banner | `@panphora/bevel/react/persistent-action-banner` | ActionBanner (Root, Dismiss, Body, Icon, Actions, Observation, Meta, Head, Count, Progress, Status) | [Example](../tests/react/consumer/catalog-cases.tsx#L105) |
226
226
  | save-status-indicator | `@panphora/bevel/react/save-status-indicator` | SaveStatus; SavedNotification | [Example](../tests/react/consumer/catalog-cases.tsx#L106) |
227
227
  | code-block | `@panphora/bevel/react/code-block` | CodeBlock (Root, Code, Copy, Wrap, Feedback, Header, Name, Language, Meta) | [Example](../tests/react/consumer/catalog-cases.tsx#L108) |
228
- | code-editor | `@panphora/bevel/react/code-editor` | CodeEditor (Root, Tab, Name, Host, Dirty, Spacer, Toolbar, Tool, Save, Surface, Status, Meta, KeyboardBar, BarTools). CodeMirror 6 and its grammars are optional peers, so this entry is not re-exported from `@panphora/bevel/react` and a consumer that never opens it pays nothing. Give the root a definite height. | [Example](../tests/react/consumer/catalog-cases.tsx#L107) |
228
+ | code-editor | `@panphora/bevel/react/code-editor` | CodeEditor (Root, Tab, Name, Host, Dirty, Spacer, Toolbar, Tool, Save, Surface, Status, Meta, KeyboardBar, BarTools). CodeMirror 6 and its grammars are optional peers, so this entry is not re-exported from `@panphora/bevel/react` and a consumer that never opens it pays nothing. Give the root a definite height. `readOnly` keeps the editor focusable and selectable; `findRequest` in `useCodeEditor()` increases on every find request, for hosts that draw their own find field. | [Example](../tests/react/consumer/catalog-cases.tsx#L107) |
229
229
  | diff-view | `@panphora/bevel/react/diff-view` | DiffView (Root, Body, Line, Count, Layout, Hunk, Header, Controls, Footer) | [Example](../tests/react/consumer/catalog-cases.tsx#L109) |
230
230
  | activity-feed | `@panphora/bevel/react/activity-feed` | ActivityFeed (Root, Groups, More, Day, List, Item, Head, Detail, Avatar) | [Example](../tests/react/consumer/catalog-cases.tsx#L110) |
231
231
  | stat-tiles | `@panphora/bevel/react/stat-tiles` | StatTile (Root, Label, Value, Delta, Spark, Description); StatTiles | [Example](../tests/react/consumer/catalog-cases.tsx#L111) |
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "richclay",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "source": "Explicitly supplied local distribution; hashes identify the exact tested bytes, not a published release claim.",
5
5
  "files": {
6
- "richclay.js": "c332abebbe33b8ef7d50130d3f0d433ccf28f73761c6f6225c7c78120212725e",
6
+ "richclay.js": "5434545ce62960fc27611edae1ef808f29dfe31f4e8bd8c8553c4eb118d83c0c",
7
7
  "richclay.css": "fc44ab9e71f8b66950fbda5a535774c7acb97d7e54aff2521ea4122d21422d47",
8
8
  "LICENSE": "5312a8ef6ff9a844697b8b112612467efd12957e8afba098ce600dec0f8396c4",
9
9
  "THIRD-PARTY-NOTICES.md": "3659529614b62297b8336ac7f6189ef9f67425911265bff1cde8d136df21562b",
@@ -1568,7 +1568,8 @@ var RichClayBundle = (() => {
1568
1568
  "no-undo",
1569
1569
  "no-data",
1570
1570
  "freeze",
1571
- "editor-ui"
1571
+ "editor-ui",
1572
+ "live"
1572
1573
  ]);
1573
1574
  function hasPolicyToken(element, token) {
1574
1575
  if (!element || element.nodeType !== 1) return false;
@@ -1609,9 +1610,6 @@ var RichClayBundle = (() => {
1609
1610
  if ("value" in source && "value" in copy && source.tagName !== "OPTION" && type !== "checkbox" && type !== "radio" && type !== "file") copy.value = source.value;
1610
1611
  if (source.tagName === "INPUT" && (type === "checkbox" || type === "radio")) copy.checked = source.checked;
1611
1612
  if (source.tagName === "OPTION") copy.selected = source.selected;
1612
- if (source.tagName === "SELECT") {
1613
- for (let i = 0; i < source.options.length; i++) copy.options[i].selected = source.options[i].selected;
1614
- }
1615
1613
  if ("indeterminate" in source && "indeterminate" in copy) copy.indeterminate = source.indeterminate;
1616
1614
  }
1617
1615
  function matchesWithin(node, selector, boundary, inherit) {
@@ -1624,40 +1622,60 @@ var RichClayBundle = (() => {
1624
1622
  }
1625
1623
  return false;
1626
1624
  }
1627
- function importTree(source, targetDocument, capability, capabilityMatch, exclude, boundary, inherit, maps) {
1628
- if (source.nodeType === 1 && ((inherit ? hasCapability(source, capability) : matchesWithin(source, capabilityMatch, boundary, false)) || exclude && (inherit && source !== boundary ? source.matches(exclude) : matchesWithin(source, exclude, boundary, inherit)))) return null;
1629
- const copy = targetDocument.importNode(source, false);
1625
+ function excluded(source, capability, capabilityMatch, exclude, boundary, inherit) {
1626
+ return (inherit ? hasCapability(source, capability) : matchesWithin(source, capabilityMatch, boundary, false)) || !!(exclude && (inherit && source !== boundary ? source.matches(exclude) : matchesWithin(source, exclude, boundary, inherit)));
1627
+ }
1628
+ function hasOwnCapability(element, capability) {
1629
+ const definition = CAPABILITIES[capability];
1630
+ return definition.tokens.some((token) => expandsTo(element, token)) || definition.bundles.some((bundle) => hasPolicyToken(element, bundle));
1631
+ }
1632
+ var CONTROL_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA", "SELECT", "OPTION"]);
1633
+ function pairTree(source, copy, maps, drop) {
1634
+ if (source.nodeType === 1 && drop(source)) {
1635
+ maps.dropped.push(copy);
1636
+ return;
1637
+ }
1630
1638
  maps.cloneToLive.set(copy, source);
1631
1639
  maps.liveToClone.set(source, copy);
1632
- const sourceChildren = source.nodeType === 1 && source.tagName === "TEMPLATE" ? source.content : source;
1633
- const copyChildren = copy.nodeType === 1 && copy.tagName === "TEMPLATE" ? copy.content : copy;
1634
- if (sourceChildren !== source) {
1635
- maps.cloneToLive.set(copyChildren, sourceChildren);
1636
- maps.liveToClone.set(sourceChildren, copyChildren);
1637
- }
1638
- for (const child of sourceChildren.childNodes || []) {
1639
- const childCopy = importTree(child, targetDocument, capability, capabilityMatch, exclude, boundary, inherit, maps);
1640
- if (childCopy) {
1641
- copyChildren.appendChild(childCopy);
1642
- if (child.nodeType === 1) copyControlState(child, childCopy);
1643
- }
1644
- }
1645
- if (source.nodeType === 1) copyControlState(source, copy);
1646
- return copy;
1647
- }
1648
- function createContentView(context, { capability = "data", exclude = null, inherit = true } = {}) {
1640
+ let sourceChildren = source;
1641
+ let copyChildren = copy;
1642
+ if (source.nodeType === 1) {
1643
+ if (CONTROL_TAGS.has(source.tagName)) maps.controls.push(source, copy);
1644
+ if (source.tagName === "TEMPLATE") {
1645
+ while (copy.firstChild) copy.removeChild(copy.firstChild);
1646
+ sourceChildren = source.content;
1647
+ copyChildren = copy.content;
1648
+ maps.cloneToLive.set(copyChildren, sourceChildren);
1649
+ maps.liveToClone.set(sourceChildren, copyChildren);
1650
+ }
1651
+ } else if (source.nodeType !== 11) return;
1652
+ let c = copyChildren.firstChild;
1653
+ for (let s = sourceChildren.firstChild; s && c; s = s.nextSibling) {
1654
+ pairTree(s, c, maps, drop);
1655
+ c = c.nextSibling;
1656
+ }
1657
+ }
1658
+ function createContentView(context, { capability = "data", exclude = null, inherit = true, provenance = null } = {}) {
1649
1659
  if (!context) throw new TypeError("createContentView requires a DOM context");
1650
1660
  const sourceDocument = context.nodeType === 9 ? context : context.ownerDocument;
1651
1661
  if (!sourceDocument?.implementation?.createHTMLDocument) {
1652
1662
  throw new TypeError("createContentView requires an HTML DOM implementation");
1653
1663
  }
1654
1664
  const inertDocument = sourceDocument.implementation.createHTMLDocument("");
1655
- const cloneToLive = /* @__PURE__ */ new WeakMap();
1665
+ const cloneToLive = provenance || /* @__PURE__ */ new WeakMap();
1656
1666
  const liveToClone = /* @__PURE__ */ new WeakMap();
1657
1667
  const sourceRoot = context.nodeType === 9 ? context.documentElement : context;
1658
1668
  if (exclude) inertDocument.documentElement.matches(exclude);
1659
1669
  const capabilityMatch = capabilitySelector(capability);
1660
- const root = importTree(sourceRoot, inertDocument, capability, capabilityMatch, exclude, sourceRoot, inherit, { cloneToLive, liveToClone });
1670
+ const drop = inherit ? (el) => hasOwnCapability(el, capability) || !!(exclude && el.matches(exclude)) : (el) => excluded(el, capability, capabilityMatch, exclude, sourceRoot, false);
1671
+ let root = null;
1672
+ if (sourceRoot.nodeType !== 1 || !excluded(sourceRoot, capability, capabilityMatch, exclude, sourceRoot, inherit)) {
1673
+ root = inertDocument.importNode(sourceRoot, true);
1674
+ const maps = { cloneToLive, liveToClone, dropped: [], controls: [] };
1675
+ pairTree(sourceRoot, root, maps, drop);
1676
+ for (const node of maps.dropped) node.remove();
1677
+ for (let i = 0; i < maps.controls.length; i += 2) copyControlState(maps.controls[i], maps.controls[i + 1]);
1678
+ }
1661
1679
  if (context.nodeType === 9 && root) {
1662
1680
  inertDocument.replaceChild(root, inertDocument.documentElement);
1663
1681
  liveToClone.set(context, inertDocument);
@@ -1768,13 +1786,16 @@ var RichClayBundle = (() => {
1768
1786
  return clone.innerHTML;
1769
1787
  }
1770
1788
  var instances = /* @__PURE__ */ new WeakMap();
1771
- var autoInitWindows = /* @__PURE__ */ new WeakSet();
1772
- var watchedWindows = /* @__PURE__ */ new WeakSet();
1789
+ var autoInitWindows = /* @__PURE__ */ new WeakMap();
1790
+ var watchedWindows = /* @__PURE__ */ new WeakMap();
1773
1791
  var globalRegistry = createDefaultRegistry();
1774
1792
  var KEEP_FOCUS = Symbol("richclay-keep-focus");
1775
1793
  var ROOT_TRANSFER_EVENTS = ["cut", "paste", "drop"];
1776
1794
  var MODIFIER_ORDER = ["Alt", "Ctrl", "Meta", "Shift"];
1777
1795
  var dialogSeq = 0;
1796
+ function isManagedByClayJS(win) {
1797
+ return typeof win.clay?.mode === "string";
1798
+ }
1778
1799
  var defaultOptions = {
1779
1800
  toolbar: "standard",
1780
1801
  toolbarContainer: null,
@@ -1868,23 +1889,36 @@ var RichClayBundle = (() => {
1868
1889
  return elements.map((element) => new _RichClay(element, options));
1869
1890
  }
1870
1891
  static autoInit(win = typeof window !== "undefined" ? window : void 0) {
1871
- if (!win || !win.document || autoInitWindows.has(win)) return;
1872
- autoInitWindows.add(win);
1892
+ if (!win || !win.document) return () => {
1893
+ };
1894
+ const existing = autoInitWindows.get(win);
1895
+ if (existing) return existing;
1896
+ let cancelled = false;
1897
+ let stopWatching = null;
1873
1898
  const run = () => {
1899
+ if (cancelled || isManagedByClayJS(win)) return;
1874
1900
  if (shouldUseHyperclay({}, win) && isHyperclayEditMode(win)) {
1875
1901
  _RichClay.init();
1876
- _RichClay.watch(win);
1902
+ stopWatching = _RichClay.watch(win);
1877
1903
  }
1878
1904
  };
1905
+ const stop = () => {
1906
+ cancelled = true;
1907
+ stopWatching?.();
1908
+ };
1909
+ autoInitWindows.set(win, stop);
1879
1910
  if (win.document.readyState === "loading") {
1880
1911
  win.document.addEventListener("DOMContentLoaded", run, { once: true });
1881
1912
  } else {
1882
1913
  run();
1883
1914
  }
1915
+ return stop;
1884
1916
  }
1885
1917
  static watch(win = typeof window !== "undefined" ? window : void 0, options = {}) {
1886
- if (!win || !win.document || watchedWindows.has(win)) return;
1887
- watchedWindows.add(win);
1918
+ if (!win || !win.document) return () => {
1919
+ };
1920
+ const existing = watchedWindows.get(win);
1921
+ if (existing) return existing;
1888
1922
  const mount = (element) => {
1889
1923
  if (!isMountable(element)) return;
1890
1924
  if (!instances.has(element) && !conflictsWithExistingEditor(element)) new _RichClay(element, options);
@@ -1913,12 +1947,21 @@ var RichClayBundle = (() => {
1913
1947
  });
1914
1948
  });
1915
1949
  });
1950
+ let stopped = false;
1951
+ const stop = () => {
1952
+ if (stopped) return;
1953
+ stopped = true;
1954
+ observer.disconnect();
1955
+ if (watchedWindows.get(win) === stop) watchedWindows.delete(win);
1956
+ };
1916
1957
  observer.observe(win.document.documentElement || win.document, {
1917
1958
  childList: true,
1918
1959
  subtree: true,
1919
1960
  attributes: true,
1920
1961
  attributeFilter: ["editable", "clay-editable", "data-richclay", "richclay"]
1921
1962
  });
1963
+ watchedWindows.set(win, stop);
1964
+ return stop;
1922
1965
  }
1923
1966
  static registerButton(def) {
1924
1967
  validateButton(def);
@@ -2001,18 +2044,18 @@ var RichClayBundle = (() => {
2001
2044
  editor._mutation?.takeRecords();
2002
2045
  },
2003
2046
  onMutations: (records, editor) => {
2004
- const excluded = (node, capability) => {
2047
+ const excluded2 = (node, capability) => {
2005
2048
  const boundary = closestWithCapability(node, capability);
2006
2049
  return !!boundary && boundary !== this.element && this.element.contains(boundary);
2007
2050
  };
2008
2051
  let authored = false;
2009
2052
  let local = false;
2010
2053
  for (const record of records) {
2011
- if (excluded(record.target, "history")) continue;
2054
+ if (excluded2(record.target, "history")) continue;
2012
2055
  const nodes = record.type === "childList" ? [...record.addedNodes, ...record.removedNodes] : [record.target];
2013
2056
  for (const node of nodes) {
2014
- if (excluded(node, "history")) continue;
2015
- if (excluded(record.target, "undo") || excluded(node, "undo")) local = true;
2057
+ if (excluded2(node, "history")) continue;
2058
+ if (excluded2(record.target, "undo") || excluded2(node, "undo")) local = true;
2016
2059
  else authored = true;
2017
2060
  }
2018
2061
  }