@ai-matrx/rich-editor 0.5.7 → 0.5.9
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 +16 -2
- package/dist/editor/RichEditorImpl.d.ts +7 -1
- package/dist/editor/RichEditorImpl.js +3 -1
- package/dist/in-place/EditInPlace.d.ts +14 -4
- package/dist/in-place/EditInPlace.js +52 -12
- package/dist/in-place/InPlaceEditor.d.ts +2 -0
- package/dist/in-place/InPlaceEditor.js +16 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.5.
|
|
3
|
+
## 0.5.9
|
|
4
|
+
|
|
5
|
+
- `hasEditInPlaceTarget()`: a right-click menu offers "Edit" only over in-place content.
|
|
6
|
+
|
|
7
|
+
Consumer action: none.
|
|
8
|
+
|
|
9
|
+
## 0.5.8
|
|
4
10
|
|
|
5
11
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
6
|
-
`git diff npm/rich-editor/v0.5.
|
|
12
|
+
`git diff npm/rich-editor/v0.5.7..npm/rich-editor/v0.5.8 -- apps/shared/rich-editor`).
|
|
7
13
|
No source changes intended and no consumer action required.
|
|
8
14
|
|
|
15
|
+
## 0.5.7
|
|
16
|
+
|
|
17
|
+
- 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).
|
|
18
|
+
- The opened editor scrolls fully into view (`scrollEditorIntoView`: nearest, or top-aligned when taller than the viewport).
|
|
19
|
+
- `RichEditor formatRow="own-row"`: row 1 = view switch + Save / Cancel, row 2 = the formatting toolbar alone; the in-place editor uses it.
|
|
20
|
+
|
|
21
|
+
Consumer action: a host whose only door was the double-click wires its Edit icon / right-click "Edit" to `openEditInPlaceFromLastInteraction()`.
|
|
22
|
+
|
|
9
23
|
## 0.5.5
|
|
10
24
|
### Images in Write after a reload; image paste in textareas; checklist Enter and `- ` (2026-10-08)
|
|
11
25
|
|
|
@@ -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
|
|
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,19 @@ 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";
|
|
11
|
+
/** Did the person just press in / right-click an edit-in-place zone (a menu offers "Edit" only then)? */
|
|
12
|
+
export declare function hasEditInPlaceTarget(maxAgeMs?: number): boolean;
|
|
9
13
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
14
|
+
* Open the edit-in-place editor the person last pressed in or right-clicked (within `maxAgeMs`):
|
|
15
|
+
* the door for a bar's Edit icon and a right-click menu's "Edit". False when none is on screen.
|
|
16
|
+
*/
|
|
17
|
+
export declare function openEditInPlaceFromLastInteraction(maxAgeMs?: number): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The opening half, for hosts that keep their own swap (the AI answer). Spread `readProps` on the
|
|
20
|
+
* element that holds the rendered text. A double-click never opens it (it selects text); the
|
|
21
|
+
* zone opens on `EDIT_IN_PLACE_EVENT` (`openEditInPlaceFromLastInteraction`).
|
|
12
22
|
*/
|
|
13
23
|
export declare function useInPlaceTrigger({ canEdit, onOpen }: {
|
|
14
24
|
canEdit: boolean;
|
|
@@ -16,8 +26,8 @@ export declare function useInPlaceTrigger({ canEdit, onOpen }: {
|
|
|
16
26
|
}): {
|
|
17
27
|
readProps: {
|
|
18
28
|
ref: import("react").RefObject<HTMLDivElement | null>;
|
|
19
|
-
onDoubleClick: (event: MouseEvent<HTMLDivElement>) => void;
|
|
20
29
|
"data-edit-in-place": "read";
|
|
30
|
+
"data-edit-in-place-door": string;
|
|
21
31
|
} | {
|
|
22
32
|
ref: import("react").RefObject<HTMLDivElement | null>;
|
|
23
33
|
"data-edit-in-place": "off";
|
|
@@ -1,25 +1,62 @@
|
|
|
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 hasEditInPlaceTarget(maxAgeMs = 15e3) {
|
|
29
|
+
const last = state.last;
|
|
30
|
+
return !!last && last.zone.isConnected && Date.now() - last.at <= maxAgeMs && last.zone.getAttribute("data-edit-in-place") === "read";
|
|
31
|
+
}
|
|
32
|
+
function openEditInPlaceFromLastInteraction(maxAgeMs = 15e3) {
|
|
33
|
+
const last = state.last;
|
|
34
|
+
if (!last || !last.zone.isConnected || Date.now() - last.at > maxAgeMs) return false;
|
|
35
|
+
if (last.zone.getAttribute("data-edit-in-place") !== "read") return false;
|
|
36
|
+
last.zone.dispatchEvent(new CustomEvent(EDIT_IN_PLACE_EVENT, { detail: { x: last.x, y: last.y } }));
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
8
39
|
function useInPlaceTrigger({ canEdit, onOpen }) {
|
|
9
40
|
const ref = useRef(null);
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
41
|
+
const onOpenRef = useRef(onOpen);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
onOpenRef.current = onOpen;
|
|
44
|
+
});
|
|
45
|
+
useEffect(() => {
|
|
14
46
|
const root = ref.current;
|
|
15
|
-
if (!root) return;
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
47
|
+
if (!canEdit || !root) return;
|
|
48
|
+
installTracking();
|
|
49
|
+
const open = (event) => {
|
|
50
|
+
const detail = event.detail;
|
|
51
|
+
const point = detail && detail.x !== null && detail.y !== null ? domPointAt(document, detail.x, detail.y) : null;
|
|
52
|
+
const caret = point ? caretContextAt(root, point.node, point.offset) : null;
|
|
53
|
+
onOpenRef.current({ caret, height: root.getBoundingClientRect().height });
|
|
54
|
+
};
|
|
55
|
+
root.addEventListener(EDIT_IN_PLACE_EVENT, open);
|
|
56
|
+
return () => root.removeEventListener(EDIT_IN_PLACE_EVENT, open);
|
|
57
|
+
});
|
|
21
58
|
return {
|
|
22
|
-
readProps: canEdit ? { ref,
|
|
59
|
+
readProps: canEdit ? { ref, "data-edit-in-place": "read", "data-edit-in-place-door": "" } : { ref, "data-edit-in-place": "off" }
|
|
23
60
|
};
|
|
24
61
|
}
|
|
25
62
|
function EditInPlace({
|
|
@@ -67,6 +104,9 @@ function EditInPlace({
|
|
|
67
104
|
return /* @__PURE__ */ jsx("div", { ...readProps, className: cn(canEdit && "cursor-text", readClassName), children });
|
|
68
105
|
}
|
|
69
106
|
export {
|
|
107
|
+
EDIT_IN_PLACE_EVENT,
|
|
70
108
|
EditInPlace,
|
|
109
|
+
hasEditInPlaceTarget,
|
|
110
|
+
openEditInPlaceFromLastInteraction,
|
|
71
111
|
useInPlaceTrigger
|
|
72
112
|
};
|
|
@@ -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.
|
|
3
|
+
"version": "0.5.9",
|
|
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,12 +90,12 @@
|
|
|
90
90
|
"@tiptap/suggestion": "^3.31.3",
|
|
91
91
|
"github-slugger": "^2.0.0",
|
|
92
92
|
"lucide-react": "^1.48.0",
|
|
93
|
+
"@ai-matrx/agents": "latest",
|
|
93
94
|
"@ai-matrx/alchemy": "latest",
|
|
94
95
|
"@ai-matrx/content-ir": "latest",
|
|
95
96
|
"@ai-matrx/design-system": "latest",
|
|
96
|
-
"@ai-matrx/print": "latest",
|
|
97
97
|
"@ai-matrx/kit": "latest",
|
|
98
|
-
"@ai-matrx/
|
|
98
|
+
"@ai-matrx/print": "latest",
|
|
99
99
|
"@ai-matrx/rich-content": "latest"
|
|
100
100
|
},
|
|
101
101
|
"peerDependencies": {
|