@ai-matrx/rich-editor 0.5.7 → 0.5.8

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/CHANGELOG.md CHANGED
@@ -1,11 +1,19 @@
1
1
  # Changelog
2
2
 
3
- ## 0.5.7
3
+ ## 0.5.8
4
4
 
5
5
  Automatic changed-only republish (docs/metadata drift since the last tag — see
6
- `git diff npm/rich-editor/v0.5.6..npm/rich-editor/v0.5.7 -- apps/shared/rich-editor`).
6
+ `git diff npm/rich-editor/v0.5.7..npm/rich-editor/v0.5.8 -- apps/shared/rich-editor`).
7
7
  No source changes intended and no consumer action required.
8
8
 
9
+ ## 0.5.7
10
+
11
+ - Edit in place never opens on a double-click (Arman, 2026-10-08: he double-clicks to select paragraphs). It opens from the bar's Edit icon or a right-click "Edit" through `openEditInPlaceFromLastInteraction()` (`EDIT_IN_PLACE_EVENT` on the zone; the caret lands where he right-clicked).
12
+ - The opened editor scrolls fully into view (`scrollEditorIntoView`: nearest, or top-aligned when taller than the viewport).
13
+ - `RichEditor formatRow="own-row"`: row 1 = view switch + Save / Cancel, row 2 = the formatting toolbar alone; the in-place editor uses it.
14
+
15
+ Consumer action: a host whose only door was the double-click wires its Edit icon / right-click "Edit" to `openEditInPlaceFromLastInteraction()`.
16
+
9
17
  ## 0.5.5
10
18
  ### Images in Write after a reload; image paste in textareas; checklist Enter and `- ` (2026-10-08)
11
19
 
@@ -122,6 +122,12 @@ export interface RichEditorProps {
122
122
  className?: string;
123
123
  /** Extra controls on the right of the toolbar (host actions). */
124
124
  toolbarExtras?: ReactNode;
125
+ /**
126
+ * Where the formatting toolbar sits in the full chrome: "inline" (the view row) or "own-row" — row 1
127
+ * holds the view switch + Save / Cancel, row 2 the formatting toolbar alone, with room for its buttons
128
+ * (the edit-in-place editor, Arman 2026-10-08).
129
+ */
130
+ formatRow?: "inline" | "own-row";
125
131
  /**
126
132
  * Leave the editor (an in-place editor going back to its reading view). Shows
127
133
  * a Cancel button beside Save; the host decides what to do with unsaved text.
@@ -147,4 +153,4 @@ export interface RichEditorProps {
147
153
  */
148
154
  imagePolicy?: ImagePolicyDeclaration;
149
155
  }
150
- export default function RichEditorImpl({ value, onSave, onChange, variables, onDeclareVariable, defaultView, view: controlledView, onViewChange, views: viewsProp, viewLabels, chrome, hostContextMenu, controllerRef, readOnly, placeholder, surfaceName, sourceFeature, contentSource, annotationRecord, saveLabel, className, toolbarExtras, onCancel, cancelLabel, defaultOutlineOpen, onNothingToSave, imagePolicy, }: RichEditorProps): import("react").JSX.Element;
156
+ export default function RichEditorImpl({ value, onSave, onChange, variables, onDeclareVariable, defaultView, view: controlledView, onViewChange, views: viewsProp, viewLabels, chrome, hostContextMenu, controllerRef, readOnly, placeholder, surfaceName, sourceFeature, contentSource, annotationRecord, saveLabel, className, toolbarExtras, formatRow, onCancel, cancelLabel, defaultOutlineOpen, onNothingToSave, imagePolicy, }: RichEditorProps): import("react").JSX.Element;
@@ -107,6 +107,7 @@ function RichEditorImpl({
107
107
  saveLabel = "Save",
108
108
  className,
109
109
  toolbarExtras,
110
+ formatRow = "inline",
110
111
  onCancel,
111
112
  cancelLabel = "Cancel",
112
113
  defaultOutlineOpen,
@@ -582,7 +583,7 @@ function RichEditorImpl({
582
583
  option
583
584
  );
584
585
  }) }),
585
- formatButtons,
586
+ formatRow === "inline" ? formatButtons : null,
586
587
  !compactTools && /* @__PURE__ */ jsxs(Fragment, { children: [
587
588
  /* @__PURE__ */ jsx("span", { className: "mx-1 h-5 w-px shrink-0 bg-border" }),
588
589
  tools.map((tool) => /* @__PURE__ */ jsx(ToolbarButton, { label: tool.label, active: tool.active, onClick: tool.run, disabled: tool.disabled, children: /* @__PURE__ */ jsx(tool.icon, { className: "h-4 w-4" }) }, tool.id)),
@@ -617,6 +618,7 @@ function RichEditorImpl({
617
618
  saveButton
618
619
  ] })
619
620
  ] }),
621
+ !focusMode && chrome === "full" && formatRow === "own-row" && editable && /* @__PURE__ */ jsx("div", { className: "matrx-touch-targets flex min-h-9 items-center border-b border-border bg-card/80 px-1.5 py-0.5 backdrop-blur", "data-rich-editor-format-row": "own-row", children: /* @__PURE__ */ jsx(FormatButtons, { className: "flex-1", resolve: () => handle.current?.formatTarget?.() ?? null }) }),
620
622
  findMode && view !== "preview" && /* @__PURE__ */ jsx(
621
623
  FindReplacePanel,
622
624
  {
@@ -1,4 +1,4 @@
1
- import { type MouseEvent, type ReactNode } from "react";
1
+ import { type ReactNode } from "react";
2
2
  import { type CaretContext } from "../core/caret-context.js";
3
3
  import { type InPlaceEditorProps } from "./InPlaceEditor.js";
4
4
  export interface InPlaceOpening {
@@ -6,9 +6,17 @@ export interface InPlaceOpening {
6
6
  /** The rendered view's height when it was swapped out. */
7
7
  height: number;
8
8
  }
9
+ /** The DOM event that opens the editor of the read zone it is dispatched on (detail: the point, if any). */
10
+ export declare const EDIT_IN_PLACE_EVENT = "matrx:edit-in-place";
9
11
  /**
10
- * The double-click half, for hosts that keep their own swap (the AI answer).
11
- * Spread `readProps` on the element that holds the rendered text.
12
+ * Open the edit-in-place editor the person last pressed in or right-clicked (within `maxAgeMs`):
13
+ * the door for a bar's Edit icon and a right-click menu's "Edit". False when none is on screen.
14
+ */
15
+ export declare function openEditInPlaceFromLastInteraction(maxAgeMs?: number): boolean;
16
+ /**
17
+ * The opening half, for hosts that keep their own swap (the AI answer). Spread `readProps` on the
18
+ * element that holds the rendered text. A double-click never opens it (it selects text); the
19
+ * zone opens on `EDIT_IN_PLACE_EVENT` (`openEditInPlaceFromLastInteraction`).
12
20
  */
13
21
  export declare function useInPlaceTrigger({ canEdit, onOpen }: {
14
22
  canEdit: boolean;
@@ -16,8 +24,8 @@ export declare function useInPlaceTrigger({ canEdit, onOpen }: {
16
24
  }): {
17
25
  readProps: {
18
26
  ref: import("react").RefObject<HTMLDivElement | null>;
19
- onDoubleClick: (event: MouseEvent<HTMLDivElement>) => void;
20
27
  "data-edit-in-place": "read";
28
+ "data-edit-in-place-door": string;
21
29
  } | {
22
30
  ref: import("react").RefObject<HTMLDivElement | null>;
23
31
  "data-edit-in-place": "off";
@@ -1,25 +1,58 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
- import { useLayoutEffect, useRef, useState } from "react";
3
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
4
  import { cn } from "@ai-matrx/design-system";
5
5
  import { caretContextAt, domPointAt } from "../core/caret-context.js";
6
6
  import { InPlaceEditor } from "./InPlaceEditor.js";
7
7
  const INTERACTIVE = "a, button, input, textarea, select, label, summary, [role='button'], [contenteditable], [data-edit-in-place-ignore]";
8
+ const EDIT_IN_PLACE_EVENT = "matrx:edit-in-place";
9
+ const READ_ZONE = '[data-edit-in-place="read"][data-edit-in-place-door]';
10
+ const state = globalThis[/* @__PURE__ */ Symbol.for("ai-matrx.rich-editor.in-place-last")] ??= {
11
+ last: null,
12
+ installed: false
13
+ };
14
+ function remember(event) {
15
+ const target = event.target instanceof Element ? event.target : null;
16
+ const zone = target?.closest(READ_ZONE);
17
+ if (!zone) return;
18
+ const point = event;
19
+ const onText = event.type === "contextmenu" && !target?.closest(INTERACTIVE);
20
+ state.last = { zone, x: onText ? point.clientX : null, y: onText ? point.clientY : null, at: Date.now() };
21
+ }
22
+ function installTracking() {
23
+ if (state.installed || typeof document === "undefined") return;
24
+ state.installed = true;
25
+ document.addEventListener("pointerdown", remember, true);
26
+ document.addEventListener("contextmenu", remember, true);
27
+ }
28
+ function openEditInPlaceFromLastInteraction(maxAgeMs = 15e3) {
29
+ const last = state.last;
30
+ if (!last || !last.zone.isConnected || Date.now() - last.at > maxAgeMs) return false;
31
+ if (last.zone.getAttribute("data-edit-in-place") !== "read") return false;
32
+ last.zone.dispatchEvent(new CustomEvent(EDIT_IN_PLACE_EVENT, { detail: { x: last.x, y: last.y } }));
33
+ return true;
34
+ }
8
35
  function useInPlaceTrigger({ canEdit, onOpen }) {
9
36
  const ref = useRef(null);
10
- const onDoubleClick = (event) => {
11
- if (!canEdit || event.defaultPrevented) return;
12
- const target = event.target instanceof Element ? event.target : null;
13
- if (target?.closest(INTERACTIVE)) return;
37
+ const onOpenRef = useRef(onOpen);
38
+ useEffect(() => {
39
+ onOpenRef.current = onOpen;
40
+ });
41
+ useEffect(() => {
14
42
  const root = ref.current;
15
- if (!root) return;
16
- const point = domPointAt(document, event.clientX, event.clientY);
17
- const caret = point ? caretContextAt(root, point.node, point.offset) : null;
18
- window.getSelection()?.removeAllRanges();
19
- onOpen({ caret, height: root.getBoundingClientRect().height });
20
- };
43
+ if (!canEdit || !root) return;
44
+ installTracking();
45
+ const open = (event) => {
46
+ const detail = event.detail;
47
+ const point = detail && detail.x !== null && detail.y !== null ? domPointAt(document, detail.x, detail.y) : null;
48
+ const caret = point ? caretContextAt(root, point.node, point.offset) : null;
49
+ onOpenRef.current({ caret, height: root.getBoundingClientRect().height });
50
+ };
51
+ root.addEventListener(EDIT_IN_PLACE_EVENT, open);
52
+ return () => root.removeEventListener(EDIT_IN_PLACE_EVENT, open);
53
+ });
21
54
  return {
22
- readProps: canEdit ? { ref, onDoubleClick, "data-edit-in-place": "read" } : { ref, "data-edit-in-place": "off" }
55
+ readProps: canEdit ? { ref, "data-edit-in-place": "read", "data-edit-in-place-door": "" } : { ref, "data-edit-in-place": "off" }
23
56
  };
24
57
  }
25
58
  function EditInPlace({
@@ -67,6 +100,8 @@ function EditInPlace({
67
100
  return /* @__PURE__ */ jsx("div", { ...readProps, className: cn(canEdit && "cursor-text", readClassName), children });
68
101
  }
69
102
  export {
103
+ EDIT_IN_PLACE_EVENT,
70
104
  EditInPlace,
105
+ openEditInPlaceFromLastInteraction,
71
106
  useInPlaceTrigger
72
107
  };
@@ -58,3 +58,5 @@ export declare function InPlaceEditor({ value, mode, write, close, caret, minHei
58
58
  * types or clicks, which ends it at once.
59
59
  */
60
60
  export declare function keepCaretInView(root: HTMLElement, settleMs?: number): () => void;
61
+ /** Bring the whole editor into view: "nearest" when it fits, its top aligned when it is taller than the viewport. */
62
+ export declare function scrollEditorIntoView(element: HTMLElement | null): void;
@@ -40,6 +40,14 @@ function InPlaceEditor({
40
40
  const shellRef = useRef(null);
41
41
  const controller = useRef(null);
42
42
  const dirty = draft !== openedOn;
43
+ useEffect(() => {
44
+ const frame = requestAnimationFrame(() => scrollEditorIntoView(shellRef.current));
45
+ const settled = window.setTimeout(() => scrollEditorIntoView(shellRef.current), 400);
46
+ return () => {
47
+ cancelAnimationFrame(frame);
48
+ window.clearTimeout(settled);
49
+ };
50
+ }, []);
43
51
  useEffect(() => {
44
52
  let tries = 0;
45
53
  let frame = 0;
@@ -129,6 +137,7 @@ function InPlaceEditor({
129
137
  /* @__PURE__ */ jsx(
130
138
  RichEditor,
131
139
  {
140
+ formatRow: "own-row",
132
141
  defaultView: "visual",
133
142
  defaultOutlineOpen: false,
134
143
  views: IN_PLACE_VIEWS,
@@ -209,9 +218,15 @@ function keepCaretInView(root, settleMs = 2500) {
209
218
  show();
210
219
  return stop;
211
220
  }
221
+ function scrollEditorIntoView(element) {
222
+ if (!element || typeof element.scrollIntoView !== "function") return;
223
+ const tall = element.getBoundingClientRect().height > (typeof window === "undefined" ? 0 : window.innerHeight);
224
+ element.scrollIntoView({ block: tall ? "start" : "nearest", inline: "nearest" });
225
+ }
212
226
  export {
213
227
  IN_PLACE_VIEWS,
214
228
  IN_PLACE_VIEW_LABELS,
215
229
  InPlaceEditor,
216
- keepCaretInView
230
+ keepCaretInView,
231
+ scrollEditorIntoView
217
232
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/rich-editor",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "The AI Matrx editor: Visual (Tiptap), Source (CodeMirror), Preview, the markdown formatting command layer, and edit-in-place — over @ai-matrx/rich-content. Consumers that only render never install it.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -90,13 +90,13 @@
90
90
  "@tiptap/suggestion": "^3.31.3",
91
91
  "github-slugger": "^2.0.0",
92
92
  "lucide-react": "^1.48.0",
93
- "@ai-matrx/alchemy": "latest",
94
93
  "@ai-matrx/content-ir": "latest",
95
94
  "@ai-matrx/design-system": "latest",
96
- "@ai-matrx/print": "latest",
95
+ "@ai-matrx/alchemy": "latest",
97
96
  "@ai-matrx/kit": "latest",
98
- "@ai-matrx/agents": "latest",
99
- "@ai-matrx/rich-content": "latest"
97
+ "@ai-matrx/print": "latest",
98
+ "@ai-matrx/rich-content": "latest",
99
+ "@ai-matrx/agents": "latest"
100
100
  },
101
101
  "peerDependencies": {
102
102
  "react": ">=19.0.0",